Skip to content

35. Performance and resource budgets

The question. How does this app spend time, battery, memory, data and storage, and which of them does it spend to save the others?

The third of the seven constraints (Chapter 2) is limited resources: battery, memory, storage, mobile data and processing power are all rationed. On a server a team that runs short of a resource buys more of it. On a device the resource belongs to the user, it is shared with every other app, and the amount is fixed on the day the phone was bought.

Two other constraints turn the ration into a design problem. Human attention sets a budget for time: a response must feel immediate, and a scroll must follow the finger. Platform rules make the operating system an enforcer. It ends processes that hold too much memory, limits what runs in the background, slows a device that grows hot, and shows the user a per-app account of battery, data and storage.

That account is what makes this concern unusually open to a teardown. Most chapters ask you to infer hidden structure from behavior. Here the device itself measures the app and publishes the figures in its settings. The figures do not say why an app spends what it spends. The rest of this chapter supplies the candidate reasons.

The word budget is meant literally. A careful team sets a number for each resource, such as a startup time, a download size or a daily amount of background data, measures it on real devices (Chapter 34), and treats a release that exceeds it as a defect. A less careful team has no numbers and finds out from reviews.

BudgetWhat spends itWho enforces itWhat the user sees when it is overspent
TimeWork before a frame can be drawnNobody. The user judgesSlow launch, stutter, late response to a tap
MemoryDecoded images, caches, screens kept aliveThe operating system ends the processThe app restarts when you return to it
BatteryWake-ups, the radio, location, processingThe operating system limits background workThe app high on the battery screen
DataMedia, prefetching, background refreshThe user’s plan, and data-saver modeThe app high on the data screen
StorageCaches, downloads, the local storeThe device fills upThe app high on the storage screen
App sizeCode, bundled resourcesThe store, on some networksA long download. An install that is put off
HeatSustained processing, radio and screenThe operating system slows the deviceA warm device, dropped frames, dimmed screen

Teams differ less in which techniques they know than in how they manage the budgets.

Posture 1. Unmanaged. Nothing is measured. Caches have no bound, images are loaded at full size, and background work runs whenever the code asks. The app works well on the team’s own new phones.

Posture 2. Reactive. The team fixes what users complain about. The symptoms that draw complaints, such as slow launch and battery drain, are handled. Those that do not, such as storage growth, are left.

Posture 3. Budgeted. Each resource has a number and a measurement. Caches are bounded, background work is scheduled, and releases are checked against the numbers.

Posture 4. Adaptive. The budgets change at run time with the device and its state. The app does less on a device with little memory, on a metered network, in low-power mode or when the device is warm. Chapter 36, Constrained devices and networks, extends this posture to whole classes of device and network.

PostureCachesBackground workReacts to device stateSignature
1 UnmanagedUnboundedUnscheduledNoStorage and battery figures grow without limit
2 ReactiveSome boundedReduced after complaintsNoGood on the common symptoms, poor on the rest
3 BudgetedAll boundedScheduled and batchedTo memory warningsFigures level off and stay proportionate to use
4 AdaptiveBounded per deviceDeferred by stateYesBehavior changes in low-power mode and data-saver mode

Low-power mode, in this chapter, is the device setting by which the user asks the system and its apps to use less battery.

Figure 35.1 places each budget on the universal app model. Every component spends at least one, and the operating system stands over all of them.

Figure 35.1. Where each budget is spent, and the policy that limits the spending.

The budget policy is the part that separates Posture 3 and Posture 4 from the others. It is a small piece of logic that reads the device’s state and sets limits for the components that spend.

function currentLimits(device):
limits = defaultsFor(device.class) // set per class of device, Chapter 36
if device.lowPowerMode:
limits.prefetch = off
limits.backgroundRefresh = off
limits.animation = reduced
if device.network.metered or device.dataSaver:
limits.prefetch = off
limits.mediaQuality = low
if device.thermalState is hot:
limits.frameRate = reduced
limits.mediaQuality = low
return limits
function onMemoryWarning():
memoryCache.evictAll() // cheaper than being ended
screensNotVisible.releaseViews()

An Unmanaged app has no such function. A Budgeted app has the defaults and the memory handler. An Adaptive app has all of it, and each branch has a visible effect that a probe can find.

The first time budget is the wait from the tap on the icon to the first useful screen. Chapter 7 covers it in full: the phases of a cold start, deferred initialization and lazy initialization. This chapter adds only the link to the other budgets. Work moved out of startup still has to run, and it spends battery and time later. App size also feeds startup, because more code takes longer to load.

The second time budget is the frame. A screen redraws many times a second, and each frame has a fixed deadline of a few milliseconds. A frame that misses its deadline is not drawn late. It is skipped, and the user sees the previous frame for twice as long. A run of skipped frames is stutter.

All drawing and all input handling happen on one thread, the main thread. Any work placed on it competes with the next frame. The design rule is short: nothing slow runs on the main thread. Reading from disk, parsing a response, decoding an image and computing a layout for a long text all belong elsewhere, and their results are handed back when ready.

A long list is the hardest test of the frame budget, because each row entering the screen needs layout, data and often an image, and the user is supplying new rows at the speed of a finger. Three techniques carry most of the load.

  • View recycling. A small set of row views is reused for whichever items are in sight (Chapter 15), so memory and setup cost do not grow with the list.
  • Work off the main thread. Images are decoded and sized in the background and shown when ready (Chapter 16, Images). Until then the row shows a placeholder.
  • Work ahead of need. Rows just beyond the screen are prepared before they are visible, and the next page is requested before the end is reached.

The visible result depends on which of these is missing. With all three, a fast scroll is smooth and placeholders fill in a moment later. Without work off the main thread, the scroll stutters each time a new image arrives. Without recycling, the scroll slows as the list gets longer, and memory rises until the process is ended.

A transition between screens is a short animation that must also hold its frame rate. An app that starts the animation and loads the next screen’s data on the main thread at the same time drops frames at the start of every transition. Designs avoid this by seeding the next screen from data already held, and by starting the fetch without waiting on it.

A tap needs a visible response within about a tenth of a second, even when the work behind it takes longer. A pressed state, an optimistic update or an immediate skeleton meets that budget. A control that shows nothing until the network answers does not.

Three things dominate a client’s memory: decoded images, caches of data, and screens kept alive beneath the current one. Images matter most. An image occupies memory in proportion to its pixel dimensions, not its file size, so one full-resolution photo shown in a small row can cost as much as hundreds of rows of text. Loading images at the size they are displayed is the largest single saving.

A memory cache is bounded by a size, and its entries are evicted when the bound is reached (Chapter 11). The bound is a direct trade between memory and time: a larger cache means fewer reloads and more risk.

A device has no way to grow its memory, so the operating system reclaims it from apps. The sequence is the same everywhere. First it warns running apps, which are expected to release what they can rebuild. Then it ends background processes, starting with those that hold the most memory or were used longest ago. Last, it ends the foreground app if that app alone exceeds its limit, which the user sees as a crash.

This makes memory use visible in an indirect way. An app that holds a lot of memory while in the background is ended sooner. When you return, there is no process, and you get a cold start where a resume was possible.

Figure 35.2. Memory held in the background decides whether a return is a resume or a cold start.

What happens after that cold start belongs to Chapter 8: an app with state restoration returns you to your screen, and one without it shows the home screen. The two chapters meet here. How often the app restarts on return is a memory question. What you see when it does is a state question.

Read this signal with care. The operating system’s choice depends on everything else running on the device, on how much memory the device has, and on how long you were away. A single restart proves little. A consistent difference between two apps on the same device, under the same use, is evidence.

Battery is spent by hardware, and an app spends it by keeping hardware awake. Four parts account for almost all of it.

The radio is not charged by the byte. It is charged by the time it spends in its high-power state. After any transfer, however small, the radio stays in that state for several seconds in case more follows, and only then drops back. A one-kilobyte request every minute therefore costs far more than the same sixty kilobytes sent at once.

The saving design is to bunch network use. Requests that are not urgent are held and sent together, with batching, alongside traffic that would have woken the radio anyway. Polling is the opposite design, and frequent polling is the classic cause of battery drain (Chapter 14). A platform push costs the app nothing while it waits, because the operating system keeps one shared connection for all apps.

A device that is idle sleeps deeply and uses very little. Each event that wakes it, such as a timer, a silent push or a location update, costs the wake itself plus whatever the app then does. The operating system therefore prefers to group background tasks from many apps into shared windows. An app that asks for work “some time in the next few hours, on power and an unmetered network” is cheap. An app that insists on an exact time is not. Chapter 23, Background work and notifications, covers what an app may ask for.

Precise positioning is one of the most expensive things an app can request, and continuous precise positioning in the background is the most expensive of all. Cheaper forms exist: a coarse position from nearby networks, updates only on significant movement, or a notice when the device enters a region. Chapter 24, Location, sensors and hardware, covers the choice. For this chapter, the point is that the battery screen often shows the result, and the device shows an indicator while location is in use.

Decoding video, running a model on the device, drawing heavy animation and keeping the screen at full brightness all cost battery while the app is in the foreground. This spending is proportionate to use, and users accept it. A dark color scheme saves power only on screens whose pixels emit their own light.

The useful split is not how much battery an app uses. It is how much it uses while you are not looking at it. Foreground use is the price of the product. Background use needs a feature to justify it: playback, navigation, a call, a sync you asked for. The battery screen on most devices shows the two separately, and P35.3 reads them.

Data is spent mostly on media. Text responses are small, and a single video can exceed a month of them. The designs that save data are therefore mostly designs about media: sizes matched to the display, qualities matched to the network (Chapter 13), and no playback the user did not ask for.

Three further spenders are worth separating.

  • Prefetching. Data fetched on a prediction is wasted when the prediction is wrong. A design limits it to an unmetered network, or to the next one or two items.
  • Background refresh. Data spent while the app is closed, so that it is current when opened.
  • Refetching. Data spent downloading again what the device already had, because a cache was too small, was evicted, or was never consulted. Validators avoid this at the cost of a small request (Chapter 11).

A metered network changes the calculation, since each megabyte has a price for the user. An app that behaves the same on cellular and on a wireless network has not made that distinction.

The storage an app occupies has three parts, and the device’s settings usually report at least two of them separately.

PartContentsGrows withSafe to delete
The appCode and bundled resourcesEach updateNo
DataThe local store, settings, downloads the user asked forUse of the appNo. The user would lose something
CacheCopies of what the backend also holdsBrowsingYes

A managed design bounds the cache by size and by age and evicts entries when the bound is reached. The storage figure for such an app climbs during the first days and then levels off. A figure that climbs without limit means some store has no bound: a cache, a log, a queue, or media the app saved without telling you.

The operating system may delete an app’s cache when the device runs low, without telling the app. A design that kept something it cannot rebuild in the cache location loses it then.

App size has two figures. The download size is what the store transfers. The installed size is larger, because the download is compressed and the installed app is not.

Size matters before the user has seen a single screen. A large download takes longer, costs data, may be held back by the store on a metered network, and may not fit on a nearly full device. Each of these loses installs. Size also matters afterwards: updates are downloads too, and a user short of storage removes the largest apps first.

Designs reduce size in four ways.

  • Ship less. Remove unused code and resources, share one implementation across features, and compress images.
  • Ship per device. The store delivers only the resources and code that match the device: one screen density, one processor type, one language. The same listing then shows different sizes on different devices.
  • Fetch on demand. Resources and whole features are left out of the install and downloaded on first use. This chapter calls them on-demand resources. The installed size starts small and grows as you explore.
  • Move to the backend. Content that could be bundled is fetched and cached instead, and logic moves into services or into server-driven UI (Chapter 31, Server-driven UI).

The last two trade size for a dependency on the network. A feature fetched on demand cannot be opened for the first time offline, and it shows a progress indicator where a bundled feature would open at once. That indicator is the signature.

Heat is the budget teams forget, because it appears only after minutes of sustained load. Processing, the radio, the screen and charging all produce it, and a phone has no fan.

When the device passes a temperature threshold, the operating system protects it. It lowers processor speed, dims the screen, slows charging and may pause the camera flash or other hardware. The app then has less processing power than it had a minute earlier, and frames it could draw comfortably start to miss their deadlines.

The operating system reports its thermal state to apps. An Adaptive design responds before the system forces the matter: it lowers its frame rate, reduces effects, drops video or camera resolution, and postpones optional work. Games, camera features, calls and navigation are where this matters. A design with no response looks fine for ten minutes and then degrades sharply. A design with a response degrades a little, earlier, and stays usable.

Almost every technique in this chapter saves one budget by spending another. A design is a position on these trades, and the device’s figures show which way the team leaned.

TechniqueSavesSpends
PrefetchingTimeData, battery, storage
A large memory cacheTimeMemory, and so more restarts on return
A large disk cacheTime, dataStorage
Bundling resources in the appTime, data at run timeApp size
On-demand resourcesApp sizeTime on first use, data later
Background refreshTime at openBattery, data
Batching requestsBatteryTime. The data is less fresh
A persistent connectionTime to deliverBattery while it is open
Higher media qualityNothing. It is the productData, battery, heat, memory
CompressionDataProcessing, and so battery and time
Work on the backendBattery, heat, app sizeData, and time on a poor network
Deferred initializationStartup timeTime and battery shortly after

Three points follow from the table.

Time is what most techniques buy, and it is the only budget with no figure in the settings. A team that optimizes what it can feel, on fast devices and unmetered networks, drifts toward spending everything else.

The right trade depends on the device and the moment. Prefetching is a good trade on a charger and an unmetered network and a bad one at five percent battery on a metered network. This is the argument for the Adaptive posture.

The trades are not hidden. An app that opens instantly with fresh content, and sits high on the data and battery screens, has prefetched and refreshed in the background. An app that is small on the storage screen and reloads everything on each visit has chosen the other side.

P35.1 to P35.4 only read figures and can run alongside a normal day of use. P35.5 to P35.8 take a few minutes each.

Open worksheet
Do
  1. Read the download size on the app's store listing and write it down.
  2. On a device where the app is installed, open the device's storage screen and read the figure for the app itself, separate from its data.
  3. If you can, read the same figure just after a fresh install on a spare device, before and after opening two or three major features for the first time.
Watch

How the installed figure compares with the listed size, whether it grows when a feature is first opened, and whether a feature shows a download indicator on first use.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Open the device's storage screen and note the app's data and cache figures.
  2. Use the app normally for a week.
  3. Note the figures every day or two.
Watch

Whether the total keeps rising, or rises and then holds.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Use the app as you normally would for a day.
  2. Open the device's battery screen and find the app.
  3. Read its share, and the split between time on screen and time in the background if the device shows it.
Watch

How large the background part is, and whether a feature you used explains it, such as playback, navigation or a call.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Note the app's figure on the device's data usage screen, with the background figure if shown.
  2. Use the app for a day on a cellular connection as you normally would. Keep a rough note of what you watched or read.
  3. Read the figures again.
Watch

Whether the total is in proportion to what you consumed, and how much was used in the background.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Open the longest list in the app that contains images or video.
  2. Scroll slowly for a few screens, then flick as fast as you can several times.
  3. Continue deep into the list, well past a hundred items if it allows.
Watch

Whether the list follows your finger, what rows look like as they enter the screen, and whether the scroll gets worse the deeper you go.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Open a screen a few levels deep in the app and scroll part way down.
  2. Leave the app without closing it.
  3. Use several other apps for ten minutes, including the camera or another demanding app.
  4. Return to the app.
  5. Repeat on two or three different days, and run the same steps on another app for comparison.
Watch

Whether the screen is exactly as you left it, or whether a splash appears first, and where you land.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. With the mode off, note how the app behaves. Look at autoplay, animations, how fresh content is on opening, and whether an unvisited item opens at once.
  2. Turn on the device's low-power mode.
  3. Force-quit the app, open it and repeat the same actions.
Watch

Any difference in autoplay, animation, media quality, freshness on opening, or a message from the app about the mode.

Caution. This probe changes a device-wide setting that affects every app. Turn the mode off when you finish.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Start with a cool device at normal room temperature.
  2. Use the app's most demanding feature continuously for fifteen minutes, such as video, a camera feature, a map or a game.
  3. Then spend fifteen minutes on its simplest screens on another day, for comparison.
Watch

Whether the device grows warm, and whether the app's quality or smoothness changes over the fifteen minutes.

Caution. Stop at once if the device shows a temperature warning or becomes uncomfortable to hold. Do not charge the device or leave it in the sun during this probe.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

SignalDesign it points toConfidence
A feature shows a download indicator the first time it is openedOn-demand resources left out of the installObserved
The same listing shows a different size on another deviceThe store delivers only the resources that match each deviceInferred
The storage figure rises for some days and then holdsBounded caches with evictionInferred
The storage figure rises without limitA cache, log or queue with no boundSpeculative
The app restarts on return more often than other apps on the same deviceHigh memory held in the background and not released on a warningInferred
The app closes on a media-heavy screenForeground memory limit exceeded. Full-size images or no view recyclingSpeculative
Fast scrolling is smooth and placeholders fill in afterwardsView recycling with decoding off the main threadInferred
Scrolling stutters as each new image arrivesDecoding or parsing on the main threadInferred
Scrolling slows deep in a long listRows or images accumulate in memoryInferred
Background battery use with no background feature in useFrequent wake-ups from polling, location or unscheduled tasksSpeculative
Data use far above what was watched or readPrefetching or autoplay not limited on a metered networkInferred
Content is already fresh on opening and background data is notableBackground refresh that spends data and battery to save timeInferred
Autoplay and animations stop in low-power modeA budget policy that reads the device's power stateObserved
Quality or frame rate steps down smoothly during long demanding useThe app responds to the thermal stateInferred
Sudden stutter and a dimmed screen after minutes of loadThe operating system throttled the device and the app did not adaptInferred
A small install whose screens all need the networkApp size saved by moving content or layout to the backendInferred
Instant opening with fresh content and high figures for data and batteryAggressive prefetching and background refreshInferred

Search the signals from every chapter in the signal lookup.

The budgets are spent on the device, and much of the saving is done on the backend.

  • Media in several sizes and qualities. Loading an image at display size requires media delivery to serve that size. Without it the client downloads the original and scales it down, spending data, memory and time together.
  • Responses cut to the screen. Over-fetching spends data and parsing time on every request. A screen-shaped response (Chapter 10) moves that cost to the backend.
  • Validators and deltas. A backend that can answer “nothing changed” cheaply lets the client refresh often without spending much data.
  • Push in place of polling. A push hint through platform push lets the client stay asleep until there is something to fetch.
  • Hosting for on-demand resources. Resources left out of the install must be served, versioned and kept available for every app version still in use.
  • Remote config for limits. Cache bounds, prefetch depth and refresh intervals set from the backend can be tuned without a release, and can differ by class of device (Chapter 30, Remote config and experiments).
  • Measurement. Budgets need numbers from real devices, which means the performance channel of Chapter 34 and a place to aggregate it.

An app that is light on the device and fast is usually backed by a backend doing more work per request. That work is invisible to you, and it is a sound Inferred claim when the client shows right-sized media and small, quick responses.

  • The app restarts almost every time you return to it. It holds a lot of memory in the background and does not release it on a warning. If your place is also lost, state restoration is missing as well.
  • The app closes while you are using a media-heavy screen. The foreground process exceeded its memory limit. Images are being held at full size, or a list is not recycling its views.
  • Scrolling slows as you go deeper into a list. Rows or images are accumulating instead of being recycled and released.
  • Stutter each time new content arrives. Parsing or image decoding runs on the main thread.
  • High background battery use with no background feature. Frequent wake-ups: polling, precise location, or tasks scheduled at exact times.
  • Storage that only ever grows. A cache, a log or a queue has no bound.
  • Data use far beyond what you watched or read. Prefetching or autoplay with no limit on a metered network.
  • The device grows hot on a simple screen. Continuous animation, a redraw loop that never idles, or work repeated in the background. Speculative, since the cause cannot be seen.
  • A feature shows a download bar the first time you open it. On-demand resources. The feature was left out of the install.
  • Frame rate falls after several minutes of a game or a call. The operating system reduced processing power for heat, and the app made no adjustment of its own.

These are the fields this chapter adds to the teardown worksheet. Record figures with the date and the amount of use behind them, since a figure without its context cannot be compared.

Open worksheet
  • An app spends seven budgets: time, memory, battery, data, storage, app size and heat. The operating system enforces most of them and publishes per-app figures for three.
  • Time is spent per frame. Slow work on the main thread shows as stutter, most clearly in a fast scroll through a media-heavy list.
  • Memory is enforced by process death. An app that holds a lot in the background restarts on return where a lighter one resumes.
  • Battery follows wake-ups and radio time, not bytes. Background use needs a feature to justify it.
  • Storage should level off. App size has a download figure and an installed figure, and on-demand resources show as growth after install and a progress indicator on first use.
  • Heat appears after minutes of load. An adaptive app lowers quality before the operating system forces it.
  • Nearly every technique saves one budget by spending another. Prefetching spends data, battery and storage to save time.
  • The device’s own settings are the instrument. They show what was spent, and the probes narrow down why.