Install
$ agentstack add skill-lynx-community-skills-rspeedy-bundle-size Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ● Dynamic code execution Used
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Rspeedy Bundle Optimization
A measure-first workflow for shrinking the bundle of a rspeedy/Lynx app. The central discipline: don't guess where the bytes are — measure, then attack the biggest lever first. Most "optimize the bundle" requests fail by starting with micro-tree-shaking when the real weight is media assets or main-thread leakage.
Verify against source — do not guess
The rspeedy / ReactLynx build behaves in non-obvious ways, and guessing is how you end up confidently wrong. When you're unsure how the build, the layers, the ES targets, or stats/rsdoctor output actually behave, read the source — don't infer from naming or memory. The whole toolchain is open source; clone or browse it and grep for the relevant plugin/util:
The toolchain layers as Rspack → Rsbuild → Rspeedy (each builds on the one before), so a behavior may live in any layer — follow it down:
- rspeedy + ReactLynx plugins (the
lynx-stackmonorepo): https://github.com/lynx-family/lynx-stack - rsbuild (the Rsbuild layer rspeedy is built on): https://github.com/web-infra-dev/rsbuild
- rspack (the underlying bundler; stats/module types, layers,
output.environment): https://github.com/web-infra-dev/rspack - rsdoctor: https://github.com/web-infra-dev/rsdoctor
(Exact file paths aren't pinned here — they move between versions. Search by symbol, e.g. the ES-target util, the stats-json plugin, the entry/layer setup.) Several facts in this skill — main=ES2019 / background=ES2015, stats.json carrying layer, the entry→two-layer split — were each confirmed by reading the source or running a real build, not assumed. Hold new claims to the same bar.
NO GUESS — trace the real module graph, never narrate an import chain
When the question is "why is X bundled / who imports Y / what's the chain from the entry to Z", you must trace the actual module graph — never describe a plausible chain from package.json, intuition, or a half-answer like "it comes in via the dependency tree, scope-hoisted into ./entry.tsx + 4630 modules." That + N modules is the bundler hiding the edge from you, not an answer. To get the real edges:
- Build with
tools.rspacksettingoptimization.moduleIds: 'named'ANDoptimization.concatenateModules: false. Scope-hoisting (concatenateModules: true, the prod default) collapses importer edges — every hoisted module's importer is reported as the whole concatenated group ("./entry.tsx + N modules"), so you literally cannot see who imports what. Disabling it is mandatory for chain tracing (it inflates size substantially — ~+70% observed on one project, varies by module graph — so it's analysis-only; revert after). De-concat also disables__split__, so components already split off the main thread reappear inflated inmain-thread.js— cross-check any "huge popup/modal" against the real build before calling it a lazy-load candidate (see [rspeedy-gotchas.md §11](references/rspeedy-gotchas.md)). - Read the actual importer edges from
stats.json'sreasons[].moduleName(each module lists the modules that import it). On a big multi-entry build this file can reach hundreds of MB to ~1 GB — too big forreadFileSync(ERR_STRING_TOO_LONG); stream it (grep/readline). (A single-page build with the default stats preset is far smaller — onlystats: { all: true }or a many-page merge blows it up; see [measure-with-rsdoctor.md](references/measure-with-rsdoctor.md).) - Walk the chain hop by hop and report the exact file at each hop. If you can't back a hop with a real reason edge, you don't have the chain — say so, don't invent it.
Real example this rule comes from: an app's 1388-icon bloat looked like "app → business component lib → UI lib → icon barrel" — but the app imported none of those packages directly (0 hits in src/); only the de-concat reasons revealed the true edges. A narrated chain would have pointed the owner at the wrong file. A dependency chain without real reason-edges behind it is a guess; do not present guesses as chains.
When to use this skill
- "帮我优化一下包体积" / "reduce the bundle size of this rspeedy app"
- "为什么我的包这么大" / "what is making this bundle large"
- After a build, the user wants the size broken down and a prioritized plan
- Deciding whether tree-shaking is even worth it, vs media compression vs main-thread cleanup
A distinct cause of an oversized bundle is mangle failure — readable module-prefixed names (common_EVENT_NAME, utils_toSomething) surviving in the production chunk, usually from eval() poisoning under ModuleConcatenation. It's covered as part of the JS lever in [references/three-levers.md](references/three-levers.md) §Lever 2.
When NOT to use this skill
This is the bundle-size / .lynx.bundle lever only — it optimizes shipped bytes, not anything else. Route elsewhere for:
- Runtime / render performance (jank, slow first paint, frame drops) — a different concern; this skill doesn't touch execution speed.
- JS memory / heap leaks — use
lynx-js-heap-snapshot-analysis. - **Directive semantics** (
'background only'accepted forms / behavior) — defer toreactlynx-best-practices; this skill only applies the directive as a size lever. - Deep
rsdoctor-data.jsonforensics (per-module retained-set, duplicate trees) — usersdoctor-analysis; this skill only summarizes it. - TS types / tsconfig →
lynx-typescript. Decoding the tasm binary format →lynx-tasm-codec.
Related skills
If you have access to them, they go deeper on adjacent problems:
rsdoctor-analysis— deep analysis ofrsdoctor-data.json(tree-shaking retained-modules, duplicate packages, side-effects). Use it for the heavy rsdoctor querying this skill only summarizes. Public: https://github.com/rstackjs/agent-skills/tree/main/skills/rsdoctor-analysisreactlynx-best-practices— the authority on the dual-thread directives, incl. the'background only'/'background-only'function directive (see itsdetect-background-onlyrule). Defer to it for directive semantics and accepted forms rather than re-deriving them here.rushstack-best-practices— use in Rush monorepos (detectrush.json/common/config/rush). Rush repos should normally userush,rushx, orrush-pnpm, not rawpnpm. Public: https://github.com/microsoft/rushstack/blob/main/skills/rushstack-best-practices/SKILL.md
Build-system awareness
Before measuring, identify the repo's build orchestrator and use its native command surface rather than assuming pnpm build:
- Rush monorepo — if
rush.jsonorcommon/config/rush/exists, use the Rush skill. Run project scripts withrushxfrom the project dir, or scoped repo commands such asrush build --to .; userush-pnpmonly when a pnpm command is truly necessary. - Custom wrapper tooling — if scripts or config wrap
rspeedybehind a company/monorepo CLI, prefer that documented entrypoint over callingrspeedy/pnpmdirectly. - Plain pnpm/npm project — fall back to
pnpm/npmdirectly when the repo is not Rush-managed and has no wrapper.
Do not blindly run pnpm build. First read package.json, root workspace files, and any existing project docs; then choose the narrowest command that builds the target rspeedy app while preserving the repo's environment setup. If the project pins Node, use that version via your Node version manager (e.g. fnm exec --using= -- ... or nvm exec ...).
Triage first when you have many projects (don't build a whole fleet)
The per-project workflow below is measure-first — but if you're handed a fleet ("optimize our Lynx bundles", a dashboard of 50+ apps), you can't build them all to decide where to look. Run a cheap source-level triage to rank which projects/levers deserve a real build, then measure-first the winners. A triage hit means "measure here", not "confirmed bloat".
The scanner [references/assets/scan-levers.mjs](references/assets/scan-levers.mjs) walks each project's source (no build) and ranks by unguarded background-only leak, missing sideEffects, inline/large media, and duplicate deps:
node references/assets/scan-levers.mjs --glob # rank a fleet
node references/assets/scan-levers.mjs --json out.json
Read [references/cross-project-triage.md](references/cross-project-triage.md) for how to interpret it and its traps (duplicate-dep counts are dominated by the dev toolchain and mostly noise; source presence ≠ main-thread presence; a telemetry signature over-counts a property name). Empirically across a real ~370-project fleet: ~74% have background-only leak signal in source, ~62% mostly unguarded, ~99% miss sideEffects — telemetry + jsb/native glue is referenced almost everywhere and almost always unguarded.
Telemetry/logging is background-only only when it's jsb-backed — a console-backed logger (console.*) runs on the main thread too, so confirm the underlying impl before treating a telemetry call as strippable. When it is jsb-backed it's a broadly-relevant Lever-3 signal — but measure before acting on it. The blind strip ([plugin-strip-mt-telemetry.ts](references/assets/plugin-strip-mt-telemetry.ts) + [loader](references/assets/strip-mt-telemetry-loader.cjs), method list customizable) netted only −3.9 kB (−0.08%) on a real project because the logger imports survived (the last-reference rule), so treat it as a measurement aid, not the fix (see [three-levers.md](references/three-levers.md) §Lever 3) — not a "drop it in and win" lever. By contrast one config line — extractStr: true — netted −1.88% to −5.99% across a 6-project fleet round (up to −1.29 MB on a 64-page string-heavy app; ~0 on thin demos or apps with little duplicated string content). So on a string-heavy app not already using extractStr, just turning it on is the highest-ROI first move — far above hand-chasing telemetry leaks. (Just set extractStr: true; tuning the strLength threshold is generally not recommended.)
The mental model: three layers, three levers
A Lynx app bundle splits across two threads, plus non-JS assets. Size lives in three places, and the optimization technique is different for each — confusing them is the most common mistake.
| Lever | What it is | Typical size | Who can fix it | |---|---|---|---| | Media assets | images/fonts inlined or shipped | often the single biggest chunk (multi-MB) | app code — usually highest ROI | | JS — background thread | react:background layer, app logic | varies | app code — tree-shaking, dedup | | JS — main thread | react:main-thread layer, first-screen render path | should be small; leaks happen | trace → module split (markers ≈ 0); big chains are library asks |
See [references/three-levers.md](references/three-levers.md) for how to size and attack each.
Read [references/dual-thread-architecture.md](references/dual-thread-architecture.md) before optimizing if you don't already know why there are two JS layers — it has the full model and the 代码裁剪 macro table that every Lever-3 fix relies on. In short: the main thread does first-screen direct render and must stay lean; the background thread carries logic / lifecycle / state. The build emits a main-thread.js + background.js, compiled into the binary .lynx.bundle the engine ships. ReactLynx auto-shakes useEffect/componentDidMount/bindtap callbacks out of main-thread.js; when it can't decide, steer it with 'background only' or the __BACKGROUND__/__MAIN_THREAD__ macros — that mechanism is the main-thread-leakage lever.
Core workflow
Step 1 — Measure (never skip)
Get a real per-module breakdown before proposing anything. This holds even when the user already hands you rough sizes (e.g. "images ~10MB, JS ~4MB") or asks you to just do a specific change — a glance at output sizes is a starting hypothesis, not a per-module measurement. **Always confirm the breakdown with rsdoctor (or stats.json) and lead with the prioritized recommendation before committing to — or implementing — any lever**, including a change the user explicitly requested. If the asked-for change is the wrong (low-ROI) lever, say so first; only proceed with it after the measured breakdown confirms it's worth it.
The official way for rspeedy is rsdoctor via config — do not manually install @rsdoctor/rspack-plugin, and do not reverse-engineer internal forks. Add to lynx.config.ts:
tools: {
rsdoctor: {
disableClientServer: true,
brief: { writeDataJson: true },
},
}
Then build with RSDOCTOR=true. Full details, allowed config fields, and the sharded-data reconstruction trick are in [references/measure-with-rsdoctor.md](references/measure-with-rsdoctor.md).
Use the build-system-aware command from the previous section:
# Rush project script
RSDOCTOR=true rushx build
# Wrapper-tooling project (example; prefer the repo's documented command)
RSDOCTOR=true build
# Plain package script
RSDOCTOR=true pnpm build
A lighter alternative for a quick first look is rspeedy's built-in stats.json — turn it on with performance.profile: true in the config, or just build with DEBUG=rspeedy (it writes output/stats.json). It does carry a layer field per module (react:main-thread / react:background, verified on the demo), so it can separate the two threads. Prefer rsdoctor when you want per-module gzip sizes and duplicate/tree-shaking analysis for free; reach for stats.json for a fast layer-vs-size split with zero extra setup. Details in [references/measure-with-rsdoctor.md](references/measure-with-rsdoctor.md).
Step 2 — Classify the weight into the three layers
From the rsdoctor moduleGraph, group module.size.parsedSize (and gzipSize) by:
- Media / assets — non-JS modules, or asset modules
- JS
react:background—module.layercontains background - JS
react:main-thread—module.layercontains main-thread
Report the three totals first. That number alone usually decides the whole engagement — e.g. "10MB of your 14MB is images; JS tree-shaking can win you ~50KB, so start with media."
Step 3 — Pick the lever by ROI, attack in order
Default priority (override if the numbers say otherwise):
1. Media assets → biggest, easiest wins (compress, dedup, format)
2. JS background → tree-shaking, duplicate packages, deep imports
3. Main-thread leak → background-only code that leaked into render path
4. Compile-layer knobs → extractStr, CSS minify, debug-info, lazy bundle — often beats the JS tail
For each lever's concrete techniques and decision points, see [references/three-levers.md](references/three-levers.md) (Levers 1–4).
Step 4 — Know when to stop
A well-optimized project hits a wall: tree-shaking is effectively exhausted (sideEffects set correctly — or, since ~99% of fleet projects omit the flag, the minifier already did the equivalent DCE), code-level DCE yields ~0 at the bundle level, and the remaining main-thread leak lives inside SDK libraries you can't edit. Say so plainly. Don't manufacture marginal changes to look busy — report the wall and what would move it (a library-side 'background only' directive, an asset pipeline change in CI, etc.).
The structural-duplication wall (the most common shape on big pages — verified by deep-diving two real big apps). A large .lynx.bundle is usually dominated not by leaks or dead code but by a heavy resource the first-screen direct render genuinely needs, carried in BOTH layers (main-thread and background each bundle their own copy under the dual-thread model). Two real examples: one app's page was 87% i18n locale JSON (all 57 languages, in both layers); another was 62% a Pixi-like render engine (~1.4 MB in each layer) that draws the first scre
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: lynx-community
- Source: lynx-community/skills
- License: Apache-2.0
- Homepage: https://lynxjs.org/next/ai/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.