Skip to content

feat: No json as string in JS bundle - #1469

Open
retyui wants to merge 1 commit into
mainfrom
feat/retyui/raw-json
Open

retyui wants to merge 1 commit into
mainfrom
feat/retyui/raw-json

Conversation

@retyui

@retyui retyui commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

issue: #1461

AI Usage:

  • Apex helps to update docs + tests and added a new .changeset

Test plan

Screenshot 2026-09-26 at 08 49 56

@changeset-bot

changeset-bot Bot commented Sep 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 10b14b3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@callstack/repack Minor
@callstack/repack-plugin-expo-modules Minor
@callstack/repack-plugin-nativewind Minor
@callstack/repack-plugin-reanimated Minor
@callstack/repack-dev-server Minor
@callstack/repack-init Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
repack-website Ready Ready Preview Sep 26, 2026 7:09am UTC

Request Review

@dannyhw

dannyhw commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator

Automated review:

Thanks for digging into this. The perf win is real on Hermes and I was able to reproduce it, but the change needs a few fixes before it can land. Details and evidence below.

What JSONParse: false actually does

generator: { JSONParse: false } disables the JSON.parse('...') wrapping, so JSON modules are emitted as plain object literals (webpack lib/json/JsonGenerator.js, Rspack JsonGeneratorOptions: "Use JSON.parse when the JSON string is longer than 20 characters", default true). Real Re.Pack CLI output:

baseline: e.exports=JSON.parse('{"namespace0":{"key_0_601103":"日本語 …
this PR:  e.exports={namespace0:{key_0_601103:"日本語 order …

That matches what #1461 asks for, and it's also what Metro does (JsFileWrapping.jsonToCommonJS → module.exports = ${source};). But the option name enableRawJson, the JSDoc, the website page and the changeset all describe the opposite ("bundled as raw text and parsed at runtime with JSON.parse('...')"). The docs also say "This keeps the bundle smaller … since the parsed object is not inlined into the bundle", but inlining the object is exactly what this does.

Perf validation (Hermes V1 250829098.0.14, same as RN 0.86)

Release tester-app on the iOS simulator (iPhone 17). JS was bundled through the Re.Pack CLI (Rspack 1.6 + Terser), compiled with RN's hermesc -O, and swapped into a Release .app. Fixtures were a 4.5 MB locale JSON (unique, mixed-unicode strings) and a 2 MB Lottie-style JSON. Median of 12 cold launches:

baseline (JSON.parse) PR (object literal)
locale require 25.8 ms 17.5 ms
lottie require 28.1 ms 8.6 ms
total 54.0 ms 25.6 ms (−53%)
JS bytes allocated 27.6 MB 23.9 MB

Hermes VM on the host (VM built from the matching tag, bytecode from RN's hermesc, hyperfine, 30 runs):

baseline PR
locale 4.5 MB 29.8 ms 23.3 ms
locale 4.2 MB (ASCII only) 28.5 ms 18.7 ms
lottie 2 MB 61.0 ms 42.3 ms

Peak memory was about 25–30% lower with the PR.

Semantics. __proto__ keys (top-level and nested), U+2028/U+2029, \u0000, quotes and backslashes, large numbers, -0, 5e-324, integer-like key ordering, nesting, 150k keys, a 1M-element array, and top-level arrays and strings all produce output identical to Node's JSON.parse, in both variants and after Terser.

Trade-offs worth documenting

  • Dev mode gets slower. When Hermes runs the unminified bundle from source (hermes -lazy, like a dev bundle from the dev server), object literals are much slower than JSON.parse (this is the V8 rationale for webpack's default):

    • locale: 75.8 → 175.0 ms
    • ASCII locale: 62.8 → 144.2 ms
    • lottie: 81.7 → 427.9 ms (peak RSS 48 → 194 MB)
  • hermesc memory at build time:

    • 2 MB Lottie: 20 MB → 472 MB peak RSS
    • 1M-element numeric array: 87 MB → 761 MB
    • 10 MB Lottie: 59 MB → 1.67 GB, and 0.1 s → 5 s

    Metro users already live with this, but it can matter on memory-limited CI runners.

  • Build time. Terser does more work: Rspack + Terser went from 630 → 1,700 ms for about 11 MB of JSON.

  • Bytecode size can go either way. Locale shrank from 8.4 to 6.3 MB, because a single non-ASCII character makes Hermes store the whole JSON.parse string as UTF-16. Lottie grew from 2.10 to 2.41 MB.

Blocking issues

  1. Builds crash on Rspack 1.0–1.2. The peer range is @rspack/core >=1, but the json generator option only exists from Rspack 1.3.0. With the new default, 1.2.8 fails immediately:

    Error: unreachable: unknow module type: json
        at getRawGeneratorOptions (@rspack/core/dist/index.js:6869:9)
        at getRawModuleRule (...)
    
  2. The rule silently overrides users' own .json rules. The templates spread ...Repack.getAssetTransformRules() last, and the last matching type wins. For example, with { test: /\.lottie\.json$/, type: 'asset/resource' } defined earlier in module.rules:

    • without this PR, the Lottie file is emitted as a separate asset (0a5eb80f52ea67a9.json);
    • with this PR, it gets inlined as a JSON module.

    Reproduced on Rspack 1.3.0, 1.6.0 and 2.2.7, and on webpack 5.105. A user rule that runs a custom loader over .json and sets type: 'javascript/auto' would be forced back to json the same way.

  3. No effect on webpack 5.90–5.98. JSONParse was added in webpack 5.99.0, and the peer range is webpack >=5.90. On older versions the option is silently ignored, so the documented behaviour doesn't happen.

  4. The docs, JSDoc, changeset and option name are inverted (see above).

Compat matrix (with the PR rule appended last, as in the templates):

bundler result
rspack 1.0.0 / 1.2.8 crash (unknow module type: json)
rspack 1.3.0 / 1.6.0 / 2.2.7 object literal ✅, but user .json rule overridden ❌
webpack 5.90.0 / 5.98.0 ignored (still JSON.parse)
webpack 5.99.0 / 5.105.4 object literal ✅, but user .json rule overridden ❌

Suggestion

Rather than a module rule in getAssetTransformRules, set the global generator default in Re.Pack's base config (getRepackConfig, or the bundle command's config), gated on bundler support:

module: { generator: { json: { JSONParse: false } } } // only for rspack >= 1.3 / webpack >= 5.99

I tested this across the same matrix:

  • output is identical (object literals);
  • user type rules are left alone (the Lottie file stays an emitted asset);
  • users can still opt out through their own module.generator.json, since user config merges on top of the Re.Pack defaults;
  • it also covers configs that don't call getAssetTransformRules.

Consider enabling it for production/bundle builds only, so dev-server bundles keep the faster source-parse path. If you keep a user-facing option, a name that mirrors the bundler option (e.g. jsonParse) would avoid the inversion.

Minor: the updated tests rely on hard-coded indices (rules[1], rules[2]), which will break on any reordering. expect(rules[0]?.test.source).not.toContain('json') is a weak assertion. None of the tests exercise an actual build, so none of the issues above are caught.

Checks run

On the PR branch: tsc --noEmit, biome check and all 337 @callstack/repack jest tests pass. The Release iOS build and simulator runs use the Re.Pack CLI end to end.

This branch was successfully deployed

1 active deployment
Preview — 10b14b36 Deployed Sep 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants