Lesson 13 / 14
App Size Management
App size management decides what the first download contains and which parts are fetched later on demand. This lesson measures how well a feature that depends on an asset pack not delivered in the first download works.
Published Updated
In this lesson
- Explains the effect of app size on the download decision without giving ratios
- Defines the on-demand asset pack and the platform-split bundle
- Builds installed asset packs as capabilities and measures the operability of a feature
- Shows by measurement that a pack that has not been downloaded is deferred, not a budget overrun
Contents
The previous lesson covered how to steer old versions toward the new version. Every update that does the steering downloads something to the device, and what is downloaded takes up space on the device. This lesson covers how large that download needs to be and how the large parts can be left for later.
The question of the course stays the same: how well does this feature work under this condition? In this lesson the condition is which asset packs are installed on the device.
App Size and the Download Decision
Before downloading an application, the user looks at its size, and the look is not always deliberate. A user with a limited connection chooses to postpone a large application. A user with little space left on the device has to delete one of the old applications to make room for a new one. An update brings the same decision back: a large update can make the user stay on the old version, which creates the problem of the previous lesson.
No ratio or byte count is given here. The threshold at which size puts the user off depends on the connection, the market, and the kind of application, and the team has to measure it in its own field. What the lesson can say points in one direction: making the first download smaller makes the download decision lighter.
Three things contribute to size: the application’s code, the assets carried inside the application, and the runtime of the framework. A framework that works with its own rendering engine carries the engine inside the application; a framework that connects to native views through a bridge carries the bridge layer. The team does not control the share of either. What the team does control is the assets: images, fonts, sounds, additional data.
On-Demand Asset Packs
Not all assets have to arrive in the first download. Not every user uses every feature. Photo filters, additional language data, or an advanced game level are needed only by the user who wants them. These assets are split into an on-demand pack, and the pack is downloaded when the user first uses the feature.
The first encounter with an on-demand pack is a download, and a download depends on the network. Without a connection the pack does not arrive, and a feature that depends on a pack that has not arrived does not work. In operability terms this situation is called deferred: the feature waits so that it can work once the pack arrives. In M31/K06 the question was in how many samples a download budget was exceeded. If an on-demand asset pack has not been downloaded, the feature is deferred; this is not a budget overrun. An overrun is a question of size, while deferred means the pack is not yet on the device.
The fragment below shows how a pack is requested; the fragment is not measured.
// Platform code: on-demand asset pack
let request = NSBundleResourceRequest(tags: ["filters"])
request.beginAccessingResources { error in
if error == nil { showFilters() } else { keepFiltersPending() }
}
Keeping the feature pending instead of closing it on an error is a design choice. Until the pack arrives, the feature shows the user “preparing”, and it opens when the pack has been downloaded. Closing tells the user the feature does not exist; keeping it pending tells the user the feature will be ready soon.
The Platform-Split Bundle
The same application does not have to carry the same files to every device. A device’s screen density and processor architecture are known; files prepared for other densities and architectures are of no use on that device. Platforms solve this with a split bundle: from the single large package that is uploaded, the store selects the part each device needs and sends only that.
On Android, splitting is on by default when the app bundle is uploaded, and a configuration block sets which splits are produced. On iOS the store makes a similar selection itself, and the team organizes assets with tags. The fragment below shows the Android side; the fragment is not measured.
// Platform code: platform-split bundle
android {
bundle {
density { enableSplit = true }
abi { enableSplit = true }
}
}
A split bundle is a size gain, and at first sight it does not affect operability: the device receives only its own part and the features work the same. The place it affects is the download decision.
Unused Assets
A common reason an application grows is that assets that are no longer used stay in the package. When a screen is removed, its images may not go with it; when a library is updated, the old copy may stay in the package. The remedy is a habit of regular scanning: finding which asset is called from which code and removing the ones that are not called. Repeating this scan before every release keeps the size from growing silently.
The scan has a limit. If an asset is reached in code through a name string, the scan may take it for uncalled. Before deleting, it is checked whether the name is built dynamically. An asset deleted by mistake is noticed only when a user reaches that screen.
Measuring the Condition of the Pack
For the measurement, take three features. Gallery is the gallery; it needs the network and the base pack pack:base, and it is present in every install. Filters is the photo filters; it needs the network and the pack:filters capability, and without the pack it works as deferred. Stickers is the stickers; it needs pack:stickers, and without the pack it also works as deferred. The core is familiar from the first lesson and is given again in the same form in every lesson; the lesson does not touch it.
// operability.mjs — classifies how fully a feature works under a condition export const LEVELS = ["full", "degraded", "deferred", "disabled"]; const FALLBACKS = ["degraded", "deferred"]; export function feature(name, needs, fallback = {}) { return { name, needs, fallback }; } export function condition(name, has) { return { name, has }; } function judge(item, cond) { const missing = item.needs.filter((need) => !cond.has.includes(need)).sort(); let level = "full"; for (const need of missing) { const given = FALLBACKS.includes(item.fallback[need]) ? item.fallback[need] : "disabled"; if (LEVELS.indexOf(given) > LEVELS.indexOf(level)) level = given; } return { level, feature: item.name, condition: cond.name, missing }; } export function assess(features, conditions) { const records = []; for (const cond of conditions) { for (const item of features) records.push(judge(item, cond)); } return records; } export function printLevels(records) { for (const r of records) { const missing = r.missing.length === 0 ? "-" : r.missing.join(","); console.log(`${r.level}: ${r.feature} @ ${r.condition} missing=${missing}`); } } export function countLevels(records) { for (const name of LEVELS) { console.log(`${name}: ${records.filter((r) => r.level === name).length}`); } console.log(`affected: ${records.filter((r) => r.level !== "full").length}`); }
After the core comes the lesson’s helper file. packsFor(install) produces a condition from the packs in an install: the network is present in every install, and each installed pack becomes a capability with the pack: prefix. assetsAtInstall(install) adds up the number of assets in an install; this number is the lesson’s local counter and is not an input to the rule. Size is counted not in bytes but as a number of assets.
// packs.mjs — builds a condition from installed packs, adds up the asset count import { feature, condition } from "./operability.mjs"; const assetCounts = { base: 30, filters: 12, stickers: 8 }; export function packsFor(install) { return condition(install.name, ["network", ...install.packs.map((name) => `pack:${name}`)]); } export function assetsAtInstall(install) { let total = 0; for (const name of install.packs) total += assetCounts[name]; return total; } export const packFeatures = [ feature("Gallery", ["network", "pack:base"]), feature("Filters", ["network", "pack:filters"], { "pack:filters": "deferred" }), feature("Stickers", ["network", "pack:stickers"], { "pack:stickers": "deferred" }), ];
RO9. The model uses the asset count as a stand-in for size: thirty, twelve, and eight are asset counts, not bytes. An asset can be an image or a long sound file; the number does not give the size directly. The model offers a counter that is enough only to compare two installs, and it builds this operability’s rule; the model is not the framework itself.
The first measurement puts two installs side by side: an install that downloads only the base pack and an install that downloads all the packs. The asset count of each install is printed first, then the six records follow.
// main.mjs import { assess, printLevels } from "./operability.mjs"; import { packsFor, assetsAtInstall, packFeatures } from "./packs.mjs"; const installs = [ { name: "base-install", packs: ["base"] }, { name: "full-install", packs: ["base", "filters", "stickers"] }, ]; for (const install of installs) { console.log(`install: ${install.name} assets=${assetsAtInstall(install)}`); } printLevels(assess(packFeatures, installs.map(packsFor)));
install: base-install assets=30 install: full-install assets=50 full: Gallery @ base-install missing=- deferred: Filters @ base-install missing=pack:filters deferred: Stickers @ base-install missing=pack:stickers full: Gallery @ full-install missing=- full: Filters @ full-install missing=- full: Stickers @ full-install missing=-
In the base install the gallery is full; the filters and the stickers come out deferred. What is missing is the pack capability, and its fallback is defined as deferred. In the full install all three features are full, but the asset count in the first download rises from thirty to fifty. The operability gain and the growth of the first download are two faces of the same decision.
When the Pack Is Downloaded
When the user opens the filters for the first time, the pack is downloaded and the install changes. The pack capability is added to the condition, and Filters moves from deferred to full. The sticker pack has still not been downloaded, so it stays deferred.
The second measurement runs the same install before and after the pack is downloaded. The condition names differ, because two conditions cannot share a name. The gallery was filtered out because it is full in both states.
// main.mjs import { assess, printLevels } from "./operability.mjs"; import { packsFor, assetsAtInstall, packFeatures } from "./packs.mjs"; const before = { name: "base-install", packs: ["base"] }; const after = { name: "filters-downloaded", packs: ["base", "filters"] }; console.log(`download: assets ${assetsAtInstall(before)} -> ${assetsAtInstall(after)}`); const records = assess(packFeatures, [before, after].map(packsFor)); printLevels(records.filter((r) => r.feature !== "Gallery"));
download: assets 30 -> 42 deferred: Filters @ base-install missing=pack:filters deferred: Stickers @ base-install missing=pack:stickers full: Filters @ filters-downloaded missing=- deferred: Stickers @ filters-downloaded missing=pack:stickers
After the download the filters are full and the stickers stay deferred. The download adds only the capability of the pack that was downloaded; it does not affect the other packs. The number of installed assets rises from thirty to forty-two.
RO10. The model assumes the download of the pack is a single step and succeeds. In reality the download can be cut halfway, the device may have no space, or the version of the pack may not match the version of the application. The rule does not see these intermediate states; it looks only at whether the pack’s capability is in the condition.
The Bounding Measurement
The focus record of this lesson is Filters @ base-install. The bounding measurement shows two things together. First, the focus record is deferred, and two of six records are affected. Second, deferred is not a budget overrun: if a limit is set for the asset count in the first download, the install that exceeds the limit is the one with the full records, and the install that stays within the limit is the one with the deferred records.
// main.mjs import { assess, printLevels, countLevels } from "./operability.mjs"; import { packsFor, assetsAtInstall, packFeatures } from "./packs.mjs"; const installs = [ { name: "base-install", packs: ["base"] }, { name: "full-install", packs: ["base", "filters", "stickers"] }, ]; const records = assess(packFeatures, installs.map(packsFor)); printLevels(records.filter((r) => r.feature === "Filters")); countLevels(records); const allowance = 40; const within = (install) => (assetsAtInstall(install) <= allowance ? "within" : "over"); console.log(`first download assets: ${assetsAtInstall(installs[0])} ${assetsAtInstall(installs[1])}`); console.log(`budget: ${installs[0].name} ${within(installs[0])} ${installs[1].name} ${within(installs[1])}`);
deferred: Filters @ base-install missing=pack:filters full: Filters @ full-install missing=- full: 4 degraded: 0 deferred: 2 disabled: 0 affected: 2 first download assets: 30 50 budget: base-install within full-install over
The first two lines carry the lesson’s operability: the same feature, the same code; only whether the pack is installed changed, and the record moved from deferred to full. The count lines say that of six records, four are full and two are deferred; affected is two.
The last two lines show the limit of the model. The lesson’s local limit is forty assets. The base install stays within this limit and leaves the filters deferred. The full install exceeds the limit, but its three features are all full. So being deferred is not a sign of an overrun, and being full is not a sign of compliance. The rule does not see the limit; it looks only at whether the pack’s capability is in the condition.
This distinction splits the decision into two questions. Is the first download small is a question of size. Do the filters work is a question of operability. Splitting off a pack to make the size smaller turns the answer to the second question from full to deferred; this is a cost, and the team pays it knowingly.
Summary
- This lesson’s operability: deferred.
Filters @ base-installcame out deferred because the filter pack did not arrive in the first download and its fallback is defined as deferred; on the same installGallerystayed full. - Making the first download smaller makes the download decision lighter; the threshold at which size puts the user off is a value the team measures in its own field.
- An on-demand asset pack is downloaded later; the feature of a pack that has not arrived does not work, but it opens when the pack arrives. A platform-split bundle sends each device only the part it needs.
- Of six records, four were full and two were deferred; affected was two. When the pack was downloaded,
Filtersmoved to full andStickersstayed deferred. - The install that exceeded the first-download limit had all three features full; deferred is not a budget overrun.
Next Step
In this lesson the problem was that something was absent from the device, and the team could know it in advance. The last lesson of the course, Crash and Usage Monitoring, covers the unknown: what a build released into the field is doing, where it crashes, and who uses which feature. It measures the crash report that waits until the connection returns, and the condition of the monitoring itself.
To keep your progress and take notes, Log in
My notes
Log in to take notes.