May 18, 2026

Build size analysis: every byte, every build

Alexey Karimov
Alexey Karimov

Mobile apps grow. Every release adds a few kilobytes — a new dependency here, a localised string set there, a high-resolution asset for some hero image. Most teams discover this when an App Store reviewer pings them about the over-cellular download limit, or when an internal “why does our APK weigh 200 MB?” thread surfaces at the worst possible time.

Today we’re shipping Bugsee Build Size Analysis — a feature that turns app size into a first-class CI signal on the same dashboard you already use for crashes, ANRs, and session replay. It’s available today on both Android (via the 4.x Gradle plugin) and iOS (via BugseeAgent), with full feature parity across the two platforms — same data model, same dashboard, same configuration knobs — so a team shipping a React Native, Flutter, or KMP app gets one mental model regardless of which side a regression surfaces on.

The problem we’re solving

App size has the same shape as performance regressions: small, frequent, invisible at PR review time, and obvious only in aggregate. Three things make it especially painful on mobile:

  1. No structural visibility per build. Android Studio’s APK Analyzer is local-only, shows one build at a time, and produces no shareable artifact. Xcode’s App Thinning Size Report lives inside an .xcarchive that nobody on the review side has access to.
  2. No baseline to compare against. You can audit today’s build, but answering “did this PR add 4 MB?” requires you to also have yesterday’s APK on disk, byte-aligned, with the same Gradle config.
  3. No CI gate. Size regressions ship the same way feature work ships: into a release branch, into a TestFlight cut, into the store. By the time someone notices, three more PRs have piled on top.

Bugsee Build Size Analysis addresses all three on every release build, on both platforms, with no developer-time UI work.

Two layers, configured independently

The feature has two parts, with independent opt-in — the lightweight one is ON by default, the heavier one stays OFF until you ask for it.

Build info (default ON)

Every Release build automatically registers a small record with Bugsee servers carrying:

  • Bundle identifier, marketing version, build number, build configuration.
  • VCS context resolved from CI env vars (GitHub Actions, GitLab CI, Bitrise, Xcode Cloud, …) or a local git invocation: commit SHA, branch, base branch, PR number, repo URL.
  • Build machine label, plugin version, host build system + SDK versions.
  • Per-category build timings (managed-code, native, resources, packaging), plus the top-N slowest tasks.
  • Raw artifact byte count.
  • The main executable’s Mach-O LC_UUID (iOS) or injected BUILD_UUID manifest meta-data (Android) — the same identifier the runtime SDK has always reported with each crash, used to tie a crash back to the build it came from.

No artifact bytes are sent on this path. The record costs essentially nothing — one sub-1 KB JSON POST per release build — and it unlocks crash-context enrichment, build-history navigation in the dashboard, and the baseline lookup the size-check feature uses.

Size analysis (opt-in)

Flip one toggle and the same build also ships the AAB/APK/IPA bytes to Bugsee for server-side tree analysis. We run the analysis server-side rather than baking it into the build plugin so we can iterate quickly on the analyzer itself — improving heuristics, broadening detection coverage, and adding new optimisation insights without asking you to bump the SDK or the Gradle plugin every time we ship one. Storage stays bounded too: each new upload replaces the prior snapshot for the same package + configuration, and older artifact bytes are deleted (chunked upload, covered below, is what makes those incremental snapshots cheap on the wire). What you get:

  • Download vs install size. The actual numbers Google Play and the App Store will report to users — not the local archive size.
  • Per-category breakdown. Where the bytes went: DEX code, native libraries (per ABI), resources, assets, Swift runtime, app entitlements.
  • Per-file diffs vs the prior build.libusercrop.so grew by 240 KB” — with the file name, the byte delta, and the percentage.
  • Optimization insights. Server-side analysis flags common bloat patterns: unused resources, duplicated assets, near-duplicate native libs.

The CI gate: size check

Knowing about the regression is half the value. Stopping it from shipping is the other half.

An optional in-build check compares each build’s artifact_size against the most recent prior build for the same (package_id, format, build_configuration) tuple. Two independent thresholds — warning and fail — each accept either a percentage or an absolute byte count. Crossing the warning threshold prints a warning in the build log; crossing the fail threshold fails the build.

The check is deliberately conservative: negative deltas never trigger (shrinking the artifact is always a PASS, not “you didn’t decrease by enough”), and the baseline is scoped to the same configuration so cross-environment comparisons never happen. Each gate is independently optional — 0 disables it — so you can ship in dry-run mode (warn only) for a few weeks before turning on the hard fail.

On Android:

bugsee {
    buildInfo {
        sizeCheck {
            enabled.set(true)
            warningPercent.set(5.0)   // warn at +5% vs the prior build
            failPercent.set(10.0)     // fail at +10%
        }
    }
    sizeAnalysis { enabled.set(true) }
}

On iOS, the same configuration is set via scheme environment variables:

BUGSEE_SIZE_CHECK_ENABLED=1
BUGSEE_SIZE_CHECK_WARNING_PCT=5.0
BUGSEE_SIZE_CHECK_FAIL_PCT=10.0
BUGSEE_SIZE_ANALYSIS_ENABLED=1

On a PR pipeline, the reviewer now sees “+4.2% download size since main” alongside the test results — at PR review time, not at QA cut time.

Fast CI: chunked upload

Most CI runs produce builds that are 95–99% identical to the previous one — one DEX changed, one resource string flipped, one Swift file recompiled. Re-uploading the entire artifact every build wastes bandwidth and latency.

So we shipped chunked upload alongside the base feature. When enabled, the client (Gradle plugin or BugseeAgent) splits the artifact into 8 MiB content-addressed chunks, asks the server which chunks it already has, uploads only the missing ones, then posts the chunk manifest. The server stitches them back into the complete artifact — transparently to the rest of the pipeline.

On our own CI runs against the sample app:

  • First build (cold cache): 11 chunks computed, 11 uploaded, ~75 seconds wall-clock.
  • Second build (identical bytes): 11 chunks computed, 0 uploaded, ~4 seconds.
  • Small code change: typically 1–3 chunks uploaded, the rest dedup’d.

That’s 70–90% upload time savings on the common CI case. Enable on Android with bugsee { chunkedUpload.set(true) }; on iOS with BUGSEE_CHUNKED_UPLOAD=1. Any failure in the chunked path falls back to the single-PUT upload automatically, so flipping the switch can’t break uploads.

Setup: zero, one, or two prompts

We know the friction point for adding any new CI integration is the setup — wiring a Gradle DSL block here, an Xcode Run Script there, a Bitrise step over there. Two paths:

Manual. For Android, a four-line addition to your app module’s build.gradle.kts. For iOS, a one-time addition of a Run Script post-action to your scheme. The Bugsee dashboard walks you through both with copy-pasteable snippets.

AI-assisted. If you use a local AI coding assistant (Claude Code, Cursor, GitHub Copilot, Windsurf, …), paste a single prompt into it. The agent locates your build script / scheme, adds the post-action or Gradle block, asks you about each optional tweak interactively (override the configuration label, speed up CI with chunked upload, add a regression gate, …), and shows you the resulting block before saving. The whole iOS or Android setup takes about 90 seconds.

This is not a thin wrapper around the manual flow. The prompts encode the same decisions a senior engineer would make: warn about caveats before applying (Debug .apps aren’t size-comparable to Release archives, so flag that before enabling every-action registration), bind to project conventions (Kotlin DSL vs Groovy DSL, SPM vs CocoaPods script paths), and never guess credentials.

Cross-platform parity

The same data model, the same dashboard, the same opt-in semantics on both platforms. A team shipping a React Native or Flutter app gets one mental model. The CI knobs name themselves consistently:

Android (Gradle DSL)iOS (env var)
buildInfo.enabledBUGSEE_BUILD_INFO_ENABLED
buildInfo.allBuildTypesBUGSEE_BUILD_INFO_ALL_CONFIGURATIONS
(implicit on Release)BUGSEE_BUILD_INFO_ALL_ACTIONS
sizeAnalysis.enabledBUGSEE_SIZE_ANALYSIS_ENABLED
buildInfo.sizeCheck.{warning,fail}PercentBUGSEE_SIZE_CHECK_{WARNING,FAIL}_PCT
chunkedUploadBUGSEE_CHUNKED_UPLOAD

What teams get out of this

Three things, in order of how visible they are to non-engineers:

  1. An always-current size-vs-time chart on the dashboard. Per platform, per configuration. The product manager finally has a number to point at when planning a “trim the app” cycle.
  2. PR-time visibility of the size delta. Reviewers stop merging “+200 KB” patches without realising they’re +200 KB. The conversation moves earlier in the pipeline.
  3. A guardrail against the slow creep. The percent-fail gate on the release branch turns “death by a thousand cuts” into a known, deliberate exception every time it fires.

Getting started

The full setup guide is in our docs:

Or skip straight to the in-product wizard: from any application’s Builds tab, click Set up. You’ll be asked whether you want a manual walkthrough or an AI-agent prompt — either way you’re wired up in under two minutes.

Both the Android Gradle plugin (4.0.0-beta7) and the updated iOS SDK are available now from Maven Central and CocoaPods / SPM respectively. Build size analysis is included with all Bugsee plans during the public beta.