---
title: 'Launch and Scrolling Performance'
source: 'https://academia.sh/en/courses/architecture-testing-and-release/launch-and-scrolling-performance'
course: 'Architecture, Testing and Release'
language: en
updated: '2026-10-21T05:03:07+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes the deferrable part of the work done before the first frame at launch'
    - 'Explains how reusing row objects limits the number of objects created while scrolling'
    - 'Measures how taking the baseline from the faulty version turns the check inverted'
    - 'Shows why a baseline refreshed on every run leaves the check blind'
---

# Launch and Scrolling Performance

Deferring the work done at launch and reusing row objects; measuring how a performance check that compares against a recorded baseline can turn inverted or blind depending on the version the baseline was taken from.

In the previous lesson (Memory Leak Detection) the fault was an object that stayed alive after a screen had closed. In this lesson the fault is in the first two places the user sees: when the app launches and when a list is scrolled. In both the output is correct; the screen opens and the rows are drawn. The difference is in the amount of work done to reach that result.

Performance checks mostly compare against a **baseline**: a value measured in an earlier run is recorded, and the new run's value must not exceed it. This lesson's question is what the check does depending on which version the baseline was taken from.

The measurement axis is **separation** again: we apply the same check to the correct version and to the faulty version. A **separating** check passes on the correct version and fails on the faulty one. A **blind** check passes on both. A **noisy** check fails on both. An **inverted** check fails on the correct version and passes on the faulty one. The core is the same as in the previous lessons; we give it again because each lesson runs on its own.

```swift
// Separation.swift — classifies whether a check tells the correct version from the faulty one
struct Contrast<Output: CustomStringConvertible> {
    let label: String
    let correct: Output
    let faulty: Output
    let passesCorrect: Bool
    let passesFaulty: Bool

    var separation: String {
        switch (passesCorrect, passesFaulty) {
        case (true, false): return "separating"
        case (true, true): return "blind"
        case (false, false): return "noisy"
        case (false, true): return "inverted"
        }
    }

    func describe() -> String {
        let onCorrect = passesCorrect ? "pass" : "fail"
        let onFaulty = passesFaulty ? "pass" : "fail"
        print("\(label) | correct=\(correct)/\(onCorrect) faulty=\(faulty)/\(onFaulty) -> \(separation)")
        return separation
    }
}

func contrast<Output>(_ label: String, correct: Output, faulty: Output, check: (Output) -> Bool) -> Contrast<Output> {
    Contrast(label: label, correct: correct, faulty: faulty, passesCorrect: check(correct), passesFaulty: check(faulty))
}

func countSeparations(_ results: [String]) {
    for name in ["separating", "blind", "noisy", "inverted"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}
```

`Contrast` holds the output of the two versions and the check's verdict on each; the `separation` label derives from these two verdicts. `contrast` applies the same closure to both outputs, and `countSeparations` counts the four categories. **The reason for the rule:** a check's value lies not in passing but in telling the two versions apart.

## Work Done at Launch

The time from the user tapping the icon until the first frame is drawn is the app's first impression. Every piece of work that runs in this time keeps the user waiting. Not all the work done at launch is needed for the first frame: starting an analytics library, warming a cache or preparing the data of a second screen can be left until after the first frame.

```swift
// app entry — SOURCE
@main
struct ShopApp: App {
    init() {
        configureTheme()
        restoreSession()
        // Not needed for the first frame; left for later.
        // warmImageCache()
        // startAnalytics()
    }

    var body: some Scene {
        WindowGroup { RootView() }
            .task { await deferredStartup() }
    }
}
```

This fragment is not run; `configureTheme`, `restoreSession` and `deferredStartup` are not defined here. The view's `task` modifier starts an asynchronous task before the view appears; the task is suspended at its first `await` point and the rest of the deferred work does not hold up the first frame. The rule is simple: the work needed to draw the first frame goes in the entry, the rest comes later.

In real apps, launch time and scrolling smoothness are also collected from users' phones. The platform's `MetricKit` framework delivers this data to the app as daily summaries.

```swift
// collected measurements — SOURCE
import MetricKit

final class MetricsReceiver: NSObject, MXMetricManagerSubscriber {
    func didReceive(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            if let launch = payload.applicationLaunchMetrics {
                record(launch.histogrammedTimeToFirstDraw)
            }
        }
    }
}
```

The summaries give the distribution under real users' real conditions; a single measurement taken during development cannot give this. This lesson's measurements, in contrast, rest on **counted work** that does not depend on conditions: how many steps ran, how many objects were created. Time is neither measured nor printed.

## Objects Created While Scrolling

In a long list only a few rows are visible on the screen at the same time. When a row leaves the screen its object is not discarded; it is put in a pool and reused for a newly visible row. This removes the work of creating an object and allocating memory for every row. Without reuse a new object is created for every row, and the work grows as the scroll gets longer.

```swift
// row creation — SOURCE
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Row", for: indexPath)
    cell.textLabel?.text = items[indexPath.row].title
    return cell
}
```

`dequeueReusableCell(withIdentifier:for:)` takes an idle row from the pool and creates a new one if there is none. The faulty version builds a new row object every time.

The **model** below is the pure counting counterpart of this; it is not a list view. It scrolls a hundred-row list through a window where ten rows are visible at a time and counts how many row objects are created. The correct version puts a row leaving the screen into the pool and uses it for the next row. The faulty version uses no pool.

```swift
// Scroll.swift — scrolling and launch model, measurement with a baseline
struct Measured: CustomStringConvertible {
    let value: Int
    let baseline: Int
    var description: String { "\(value) baseline=\(baseline)" }
}

func cellsCreated(rows: Int, visible: Int, reuse: Bool) -> Int {
    var pool = 0
    var created = 0
    var onScreen = 0
    for _ in 0..<rows {
        if onScreen == visible {
            onScreen -= 1
            if reuse { pool += 1 }
        }
        if reuse && pool > 0 { pool -= 1 } else { created += 1 }
        onScreen += 1
    }
    return created
}

func drawnRows(rows: Int, reuse: Bool) -> Int {
    rows
}

func launchWork(deferHeavy: Bool) -> Int {
    let light = 3
    let heavy = 5
    return deferHeavy ? light : light + heavy
}
```

`cellsCreated` runs the scroll with a loop: when the window is full, the oldest row leaves the screen, and if reuse is on it goes into the pool. `drawnRows` is the number of rows drawn in both versions. `launchWork` counts the steps that run before the first frame at launch: three light steps always, five heavy steps only when they are not deferred. `Measured` holds a value together with its recorded baseline.

### The Source of the Baseline

We apply three checks to the same scroll. The first looks at the number of rows drawn. The second says "the value must equal the baseline" and takes the baseline from the **faulty** version. The third says "the value must not exceed the baseline" and takes the baseline from the **correct** version.

```swift
// main.swift — three checks, the source of the baseline
let drawn = contrast("drawn-rows", correct: drawnRows(rows: 100, reuse: true), faulty: drawnRows(rows: 100, reuse: false), check: { $0 == 100 }).describe()

let faultyBaseline = cellsCreated(rows: 100, visible: 10, reuse: false)
let equalToFaulty = contrast("equal-baseline", correct: Measured(value: cellsCreated(rows: 100, visible: 10, reuse: true), baseline: faultyBaseline), faulty: Measured(value: cellsCreated(rows: 100, visible: 10, reuse: false), baseline: faultyBaseline), check: { $0.value == $0.baseline }).describe()

let correctBaseline = cellsCreated(rows: 100, visible: 10, reuse: true)
let notAboveCorrect = contrast("within-baseline", correct: Measured(value: cellsCreated(rows: 100, visible: 10, reuse: true), baseline: correctBaseline), faulty: Measured(value: cellsCreated(rows: 100, visible: 10, reuse: false), baseline: correctBaseline), check: { $0.value <= $0.baseline }).describe()

countSeparations([drawn, equalToFaulty, notAboveCorrect])
```

```
drawn-rows | correct=100/pass faulty=100/pass -> blind
equal-baseline | correct=10 baseline=100/fail faulty=100 baseline=100/pass -> inverted
within-baseline | correct=10 baseline=10/pass faulty=100 baseline=10/fail -> separating
separating: 1
blind: 1
noisy: 0
inverted: 1
```

`drawn-rows` is blind: both versions draw a hundred rows, and the list the user sees is the same. `equal-baseline` is inverted: the baseline was taken from the faulty version, that is, a hundred objects; the correct version, with ten objects, does not equal this value, so the check fails, while the faulty version equals its own value and passes. The improvement looks red and the fault looks green. `within-baseline` is separating: the baseline was taken from the correct version, the correct version passes and the faulty version exceeds it.

An inverted check is worse than being blind, because it reverses the relation between what is measured and the baseline. A blind check says nothing; an inverted check says the wrong thing and pushes the team to roll back the correct version. This is why, when a baseline is recorded, the version it was taken from is recorded too, and "does not exceed" is said instead of "equal": a performance improvement must not be counted as an error.

**TP1.** Taking the baseline from the faulty version is a design mistake, but it happens often in practice: a team records a value without knowing about the fault and fixes it later.

**TP2.** The model measures only the number of objects created. In a real list the row's content, size and image also affect scrolling; these are not counted.

## The Bounding Measurement

The baseline has one more problem: refreshing it. To keep the check from turning red, some teams pull the baseline to the current value on every run. The check "the value must not exceed the baseline" then always passes.

The measurement below puts three things side by side: the "does not exceed" check with a fixed baseline, the same check with a baseline refreshed on every run, and the check with a fixed baseline applied to the work that runs before the first frame at launch.

```swift
// main.swift — fixed and refreshed baseline, launch
let correctCells = cellsCreated(rows: 100, visible: 10, reuse: true)
let faultyCells = cellsCreated(rows: 100, visible: 10, reuse: false)

let fixed = contrast("fixed-baseline", correct: Measured(value: correctCells, baseline: correctCells), faulty: Measured(value: faultyCells, baseline: correctCells), check: { $0.value <= $0.baseline }).describe()
let refreshed = contrast("refreshed-baseline", correct: Measured(value: correctCells, baseline: correctCells), faulty: Measured(value: faultyCells, baseline: faultyCells), check: { $0.value <= $0.baseline }).describe()

let correctLaunch = launchWork(deferHeavy: true)
let launch = contrast("pre-first-frame-work", correct: Measured(value: correctLaunch, baseline: correctLaunch), faulty: Measured(value: launchWork(deferHeavy: false), baseline: correctLaunch), check: { $0.value <= $0.baseline }).describe()

countSeparations([fixed, refreshed, launch])
```

```
fixed-baseline | correct=10 baseline=10/pass faulty=100 baseline=10/fail -> separating
refreshed-baseline | correct=10 baseline=10/pass faulty=100 baseline=100/pass -> blind
pre-first-frame-work | correct=3 baseline=3/pass faulty=8 baseline=3/fail -> separating
separating: 2
blind: 1
noisy: 0
inverted: 0
```

`fixed-baseline` is the separating check of the previous measurement. `refreshed-baseline` is blind: since the faulty version's baseline is also pulled to its own value, a hundred objects do not exceed a hundred objects. Because the check never breaks, believing that it protects something is misleading. `pre-first-frame-work` is separating: the undeferred launch runs eight steps and exceeds the fixed three-step baseline.

Refreshing the baseline is not wrong in itself; after a deliberate improvement a new baseline must be recorded. What is wrong is doing it automatically on every run. A baseline changes by a decision, not by a run.

**TP3.** The model counts "steps" and "objects". In a real launch the steps do not take equal time; a single heavy step can take longer than all eight.

**TP4.** The launch measurement counts only the work before the first frame. The work deferred until after the first frame also has a cost; deferring does not remove it.

## Common Mistakes

- Recording the baseline from a faulty or unknown version and saying "equal".
- Pulling the baseline to the current value on every run. The check never breaks.
- Doing at launch the work that is not needed for the first frame.
- Creating a new object for every row; skipping the reuse pool.
- Treating a single local measurement as representative of users' conditions.

## Summary

- The work before the first frame keeps the user waiting; only what is needed for the first frame is done in the entry, the rest is deferred.
- Reusing row objects keeps the number of objects created while scrolling at the number of visible rows.
- If the baseline is taken from the faulty version, the "equal" check becomes inverted: the improvement looks red and the fault looks green.
- If the baseline is taken from the correct version, the "does not exceed" check separates; a baseline refreshed on every run leaves the check blind.
- A baseline changes by a decision, not by a run.
- This lesson's separation: **inverted**.

## Next Step

A performance check can be written as a rule; so can code quality checks. In the next lesson, Code Quality Tools will measure whether a formatting check and a rule that scans the source text can tell the same fault apart.
