babelLoader always uses hermes-parser, unlike Metro — 90s on one prebuilt file
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- react-native, typescript
- Domain
- build-system, performance
Research direction
Start in packages/repack/src/loaders/babelLoader/babelLoader.ts:89-96 and compare its parser selection with the preset behavior described for babel-plugin-syntax-hermes-parser. Use the reported @lottiefiles/dotlottie-react file and Flow examples to verify the selection, then confirm that bundle output remains byte-identical and that non-Flow files avoid the hermes-parser slowdown.
Written by the indexing model from the issue text.
Description
The problem
babelLoader parses every non-TypeScript file with hermes-parser (packages/repack/src/loaders/babelLoader/babelLoader.ts:89-96):
const sourceAst = isTS || isTSX
? parseSync(src, babelConfig)
: hermesParser.parse(src, { babel: true, ... });
Metro does not. @react-native/babel-preset passes parseLangTypes: 'flow' to babel-plugin-syntax-hermes-parser, which skips hermes-parser when a file has no @flow pragma and lets @babel/parser handle it:
// babel-plugin-syntax-hermes-parser/dist/index.js:56
if (parseLangTypes === 'flow' && !/@flow/.test(code)) { return; }
So Re.Pack ignores the preset's own parseLangTypes, and a dependency that is free under Metro can cost minutes here.
What it cost us
We bundle the Expensify app with Re.Pack. Our cold production iOS build was ~114s, against Metro's ~83s. One file accounted for 94s of that:
@lottiefiles/dotlottie-react/dist/browser/index.js — 502 KB, 22 lines, prebuilt minified ESM, no @flow pragma.
| parser | time |
|---|---|
| hermes-parser | 90.1s |
| @babel/parser | 0.1s |
That is not the parse itself. parse(src, {babel: false}) takes 0.1s; all 88s is in hermes-parser's Hermes-AST to Babel-AST conversion, which is quadratic in the number of sibling nodes: every node replacement copies the whole sibling array. This file has one ArrayExpression with 128,834 elements. Reported upstream with a profile and a minimal repro: facebook/hermes#2158.
Because it is one serial task, it set a minimum time for the whole build no matter how many workers we gave it.
It was also hard to spot: no warning, and nothing showed up in per-loader timings. We only found it by bisecting.
Across our whole babel lane (3522 files), 2779 have no pragma. Those parse in 1.1s with @babel/parser versus 4.7s with hermes-parser — and hermes-parser fails on 2 of them.
Why not simply switch parsers
hermes-parser is still needed for the other files. @babel/parser fails on 241 files in react-native 0.86, which use Flow syntax it cannot read (as casts, component(...) types). The pragma check is the fix, not a swap.
Suggested fix
const sourceAst = isTS || isTSX
? parseSync(src, babelConfig)
: /@flow/.test(src)
? hermesParser.parse(src, { babel: true, ... })
: parseSync(src, babelConfig);
Better still, read the preset's parseLangTypes instead of hard-coding the check, so parseLangTypes: 'all' still sends everything to hermes-parser.
We have tested this
We run it in our app through the existing hermesParserPath option, which babelSwcLoader passes through. Our replacement parser checks for @flow and picks hermes-parser only then.
- bundle output is byte-identical, apart from the sourcemap filename
- build time is flat, because we had already moved that package to another loader
- the file above: 90.1s → 0.1s
Identical output on a 35 MB bundle shows the check does not change what gets built.
Happy to open a PR.
Related
#1422 covers Flow in the SWC-native lane (getJsTransformRules). This report is about the babel lane that the templates use, so they do not overlap, but both come down to how Flow is detected.
Environment
@callstack/repack@5.2.5, Rspack 2.1.3, hermes-parser@0.36.1, @babel/parser@7.29.7, react-native 0.86, Node 26.5.0, macOS arm64.
- Dominant language
- TypeScript
- Stars
- 1.9k
- Forks
- 164
- Avg merge
- 10d 13h
- Merged PRs (30d)
- 10
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from callstack/repack
-
Difficulty 3/5 1-2 days Newbie friendliness 78/100
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
-
Dev server progress goes backwards mid-compilation (non-monotonic percentage forwarded to reporters) Open
Difficulty 3/5 1-2 days Newbie friendliness 70/100
-
area:repack type:feature
Difficulty 5/5 Over a week Newbie friendliness 42/100
-
area:repack type:feature
Difficulty 5/5 Over a week Newbie friendliness 30/100
All issues in callstack/repack
Similar issues
-
blocklist removal
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
MetaMask/eth-phishing-detect#296544 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
pastelsky/bundlephobia#1122 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
category/development priority/P2 scope/file-operations scope/testing type/enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Enatega Customer and Rider app: Add-ons price is not visible to customer after order is placed. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100