Bundle Size
Bundle size directly shapes download, parse, and first-screen time, yet it is the easiest metric to overlook. A dependency upgrade, an uncompressed image, or background-only code that leaks into the first-screen render path can each inflate your output.
Unlike a regular web app, a Lynx app has a distinctive output structure because of its dual-thread architecture, and understanding that structure is the prerequisite for analyzing and optimizing size. Rspeedy is built on top of Rsbuild and Rspack, so most of their size-optimization techniques apply. This page explains the Lynx-specific output structure, then the analysis workflow and optimization levers, linking out to the deeper docs.
Understand the Lynx output structure
Dual-thread architecture: two JS layers
ReactLynx splits React across two threads (see Thinking in ReactLynx):
- Main thread: a lightweight UI driver responsible for Instant First-Frame Rendering (IFR), UI updates, and touch-following animation and gestures. It sits on the rendering critical path and must stay lean.
- Background thread: the logic and state center that handles full render, lifecycle,
setState, data requests, telemetry, and so on.
Accordingly, the build emits two JS compilation intermediates per entry, then encodes them into the final binary artifact:
The two JS layers are encoded together into a single binary .lynx.bundle, located under dist/ after a production build (see Output Files). That binary is what the Lynx engine loads, and it is the final metric to watch when measuring size.
"Main-thread output / main-thread.js" here means the compiled output of the main-thread layer. It is not the Main Thread Script, the programming model written with the 'main thread' directive for touch-following animation and gestures. Keep the two distinct.
Inspect the readable intermediates
.lynx.bundle is binary and can't be read directly. To inspect the readable main-thread.js / background.js, build with DEBUG, which keeps the intermediates under dist/.rspeedy/<entry>/:
A development build also emits these intermediates under dist/.rspeedy/; see Output Files.
Code elimination: why the main-thread output should be small
Because the main thread sits on the rendering critical path, any code only the background needs (network requests, NativeModule calls, loggers, telemetry, lifecycle, etc.) bloats both the bundle and the first-frame cost once it is bundled into the main-thread output.
ReactLynx automatically eliminates code that shouldn't run on the main thread: by default, the callbacks of useEffect / useLayoutEffect and some event handlers are removed from main-thread.js (see Thinking in ReactLynx). When the compiler can't statically decide which thread a piece of code belongs to, you mark it explicitly:
- Use the
'background only'directive to keep a function body only on the background thread. - Use the
__BACKGROUND__/__MAIN_THREAD__built-in macros to eliminate code blocks per thread.
This elimination mechanism is exactly what the "slim down the main-thread output" lever below relies on.
Analyze bundle size
The guiding principle is measure first, optimize second: use data to confirm which layer the bytes are in before deciding what to touch, never guess and edit. Break the size down across three layers: static assets, background-thread output, and main-thread output.
Use Rsdoctor
Rsdoctor is a build-analysis tool that visualizes the asset list, module dependency graph, duplicate modules, and more. Enable it in a Rspeedy-based project:
It opens an analysis page once the build finishes. Its module data carries a layer field, so you can attribute bytes to the react:main-thread and react:background layers, locate the largest modules, and trace "why was this module bundled in" through the real module graph rather than intuition. See Use Rsdoctor for the full workflow.
Use the rspeedy-bundle-size Skill
If you work with an AI coding tool that supports Agent Skills, load the rspeedy-bundle-size skill. It follows a measure-first workflow: it reads how your project builds, generates a size breakdown, attributes bytes layer by layer, and proposes optimizations in priority order. It is a good way to get an evidence-based report when you are not deeply familiar with the build internals.
Optimize bundle size
Once you have located where the size comes from, pick the lever that matches the layer.
Slim down the main-thread output (strip background-only code)
This is the Lynx-specific layer, and the first one to check. Move code only the background needs (requests / NativeModule calls / loggers / monitoring SDKs) off the render path with 'background only' or __BACKGROUND__, keeping it out of main-thread.js. If such code comes from a third-party library you can't edit, it becomes a library-side ask to add the directive at the definition site.
Trim the background-thread output
- Reduce duplicate dependencies: use Rsdoctor's duplicate-package detection to find dependencies bundled more than once, then converge them with
resolve.dedupeorpnpm dedupe. - Use lighter libraries: use Rsdoctor to find the largest dependencies and evaluate lighter alternatives (for example
dayjsinstead ofmoment), or switch to importing only what you use.
Compress static assets
Images, fonts, and media are usually the largest part of a bundle. Compress them at the source, use appropriately sized assets, and split large, non-first-screen assets out of the main bundle.
Split and lazy-load
Distinguish first-screen size from total size: the first screen only needs the main-thread output and first-screen assets, so splitting out the rest noticeably improves startup.
- Use code splitting to defer non-first-screen components until they actually render.
- Tune chunking with
splitChunks(which maps onto Rspack's code splitting). - Use External Bundle to factor out reusable parts.
Compile-layer options
extractStr: merges duplicate string literals to reduce size, especially effective for strings that are duplicated across the main-thread and background-thread layers, such as i18n text and class names.
Next Steps
For a more systematic checklist and lower-level control, continue from Rsbuild's Bundle size optimization and Rspack's optimization guide.