Skip to content

CI: Track SWC native binary size in PR stats comment#92938

Merged
sokra merged 2 commits intocanaryfrom
sokra/stats-binary-size
Apr 18, 2026
Merged

CI: Track SWC native binary size in PR stats comment#92938
sokra merged 2 commits intocanaryfrom
sokra/stats-binary-size

Conversation

@sokra
Copy link
Copy Markdown
Member

@sokra sokra commented Apr 17, 2026

What?

Adds a new "Native Binary" section to the Stats from current PR comment posted by the generate-pull-request-stats GitHub Action. The section reports the size of the SWC native binary (packages/next-swc/native/*.node) built for the PR and the change relative to the latest value recorded on canary.

Example row:

Metric Canary PR Change Trend
SWC Binary Size 113 MB 121 MB 🔴 +7.34 MB (+6%) ▁▃▅▇█

Why?

The .node binary is a sizable, user-facing artifact (it ships with next). Today there's no visibility on its size in PRs, so regressions from Rust/Cargo changes are easy to miss. Surfacing the size (and a diff vs. canary) in the existing stats comment gives immediate feedback on any change that affects the binary.

How?

  • Measurement (.github/actions/next-stats-action/src/run/index.js): After the action copies the pre-built native binary into the working dirs, sum the size of every *.node file under the action's native/ directory and store it as General.swcBinarySize on both the canary and PR stat sets.
  • Canary baseline via KV history (.github/actions/next-stats-action/src/add-comment.js): The workflow downloads a single pre-built binary and copies it into both the canary and PR checkouts, so an in-run diff would always be zero. Instead, for PR runs we override mainRepoStats.General.swcBinarySize with the most recent swcBinarySize from the Vercel KV history (the same store already used for other canary metrics). Canary runs keep the measured value, and the existing persistence path (saveToHistory / General) writes it back so future PRs see an updated baseline. If no history exists yet, the Canary column renders N/A and no change is shown.
  • Rendering: New generateNativeBinarySection emits a <details>-wrapped section styled like Bundle Sizes / All Metrics, rendered right after the Bundle Sizes block. It reuses prettify, formatChange, and generateTrendBar so formatting, thresholds, and sparklines are consistent with the rest of the comment. A tight threshold entry (swcBinarySize: { absoluteMin: 10 KB, percentMin: 0.5%, percentOnly: 0.05% }) is added so meaningful movement is flagged while tiny determinism noise is ignored.
  • Top-line summary: Because swcBinarySize lives on General, the existing generateChangeSummary automatically promotes significant changes into the headline regression/improvement table at the top of the comment, using the label SWC Binary Size.
  • Sharded runs: The workflow runs two sharded jobs (Webpack and Turbopack). The binary is bundler-independent, so both shards report the same value; aggregate-results.js merges General via Object.assign, which is correct here (last-writer-wins on an identical value).

No workflow changes are required — the existing pull_request_stats.yml already downloads the next-swc-binary artifact and copies it into the action's native/ directory.

Verification

Smoke-tested add-comment.js locally with LOCAL_STATS=1 in two scenarios:

  • No KV history configured → Canary column renders N/A, PR value renders, Change is -, section still shown.
  • Mocked KV history containing prior swcBinarySize values → full diff rendered in both the Native Binary section and the top-level significance summary, with a sparkline of the last 5 historical entries.

Closes NEXT-

sokra and others added 2 commits April 17, 2026 11:04
Adds a dedicated "Native Binary" section to the stats PR comment that
tracks the size of `packages/next-swc/native/*.node`. Because the same
binary is reused across the canary and PR checkouts in a single run, the
canary baseline is sourced from the most recent value stored in Vercel
KV history; canary runs record the measured value so the baseline stays
up-to-date. Significant changes also surface in the top summary and get
a trend sparkline.

Co-Authored-By: Claude <noreply@anthropic.com>
…ctions

Co-Authored-By: Claude <noreply@anthropic.com>
@nextjs-bot nextjs-bot added the created-by: Turbopack team PRs by the Turbopack team. label Apr 17, 2026
@nextjs-bot
Copy link
Copy Markdown
Contributor

nextjs-bot commented Apr 17, 2026

Failing test suites

Commit: 8ec5f99 | About building and testing Next.js

pnpm test test/integration/rewrites-manual-href-as/test/index.test.ts (turbopack) (job)

  • rewrites manual href/as > development mode > should allow manual href/as on index page (DD)
  • rewrites manual href/as > production mode > should allow manual href/as on index page (DD)
Expand output

● rewrites manual href/as › development mode › should allow manual href/as on index page

expect(received).toBeTruthy()

Received: false

  28 |
  29 |     expect(await browser.elementByCss('#index').text()).toBe('index page')
> 30 |     expect(await browser.hasElementByCssSelector('#modal')).toBeTruthy()
     |                                                             ^
  31 |     expect(await browser.eval('window.beforeNav')).toBe(1)
  32 |     expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
  33 |       imageId: '123',

  at Object.toBeTruthy (integration/rewrites-manual-href-as/test/index.test.ts:30:61)

● rewrites manual href/as › production mode › should allow manual href/as on index page

expect(received).toBeTruthy()

Received: false

  28 |
  29 |     expect(await browser.elementByCss('#index').text()).toBe('index page')
> 30 |     expect(await browser.hasElementByCssSelector('#modal')).toBeTruthy()
     |                                                             ^
  31 |     expect(await browser.eval('window.beforeNav')).toBe(1)
  32 |     expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
  33 |       imageId: '123',

  at Object.toBeTruthy (integration/rewrites-manual-href-as/test/index.test.ts:30:61)

@nextjs-bot
Copy link
Copy Markdown
Contributor

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 455ms 455ms ▁▁▁▁▁
Cold (Ready in log) 440ms 440ms ▂▁▁▁▂
Cold (First Request) 829ms 823ms ███▁▁
Warm (Listen) 456ms 456ms ▁▁▁▁▁
Warm (Ready in log) 440ms 439ms ▂▂▄▁▁
Warm (First Request) 338ms 340ms ▆▇█▂▆
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 455ms 455ms █▁▁▁▅
Cold (Ready in log) 440ms 441ms █▃▁▁▂
Cold (First Request) 1.925s 1.932s █▃▁▁▅
Warm (Listen) 455ms 455ms █▅▁▅▅
Warm (Ready in log) 442ms 441ms █▃▁▁▁
Warm (First Request) 1.910s 1.902s █▂▁▁▄

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 4.063s 4.033s ▂▂▁▃▃
Cached Build 4.036s 4.075s ▂▁▁▂▃
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.593s 14.618s █▃▁▁▁
Cached Build 14.729s 14.779s █▃▁▁▁
node_modules Size 494 MB 494 MB ███▁█
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
07rxhp_1_g4mu.js gzip 13.1 kB N/A -
08avva-dy02e7.js gzip 10.4 kB N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0fli3_wppnim5.js gzip 12.9 kB N/A -
0k09jwjeb-tki.js gzip 13.8 kB N/A -
0kb7_ep3r1z0_.js gzip 10.1 kB N/A -
0khj6l1dkbztz.js gzip 158 B N/A -
0kw8xgqdrilf6.js gzip 8.56 kB N/A -
0ojkk2e654xsc.js gzip 8.59 kB N/A -
0r13y5wryy_rb.js gzip 156 B N/A -
0wxpyd8r-vipl.js gzip 1.47 kB N/A -
0xy2fhla48_rd.js gzip 9.24 kB N/A -
10wqsvi2mgfmi.js gzip 9.82 kB N/A -
10x5yjviaxcoo.js gzip 155 B N/A -
16c9-6ahplfxj.js gzip 157 B N/A -
16lhqjoqbznyg.js gzip 220 B 220 B
16vepdkipri3r.js gzip 8.51 kB N/A -
17n96uu6y1pxq.js gzip 8.6 kB N/A -
18y4_8-9or0mn.js gzip 8.51 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1gq145j3kps-h.js gzip 8.62 kB N/A -
1nsh-mbn0e-se.js gzip 8.56 kB N/A -
1tsrrp1tdngti.js gzip 13.3 kB N/A -
2__-e_ym8n788.js gzip 450 B N/A -
22o6xd9_ywdu6.js gzip 233 B N/A -
25l6e629itu5j.js gzip 153 B N/A -
26ui6d5bv607a.js gzip 49.3 kB N/A -
27niq-zweblzs.js gzip 154 B N/A -
29bzq8qn1wyxo.js gzip 156 B N/A -
2jrge3u8y0nbz.js gzip 155 B N/A -
2kvj8yrfznmwx.js gzip 5.69 kB N/A -
2nrhepcp9ve2o.js gzip 70.8 kB N/A -
2qv7m7xjnokgr.js gzip 8.58 kB N/A -
2sewu64d_awot.js gzip 157 B N/A -
3_otef55yxm6_.js gzip 160 B N/A -
342ijzvrpe53h.js gzip 2.29 kB N/A -
3hiogkn179_b_.js gzip 169 B N/A -
3j78vioukb8ay.js gzip 156 B N/A -
3mm89bcl7qq09.js gzip 65.5 kB N/A -
3u73li4mpeclh.js gzip 157 B N/A -
44un3--wmqiyh.js gzip 7.61 kB N/A -
turbopack-0-..yjzq.js gzip 4.19 kB N/A -
turbopack-0h..4zvl.js gzip 4.19 kB N/A -
turbopack-0n..aaf6.js gzip 4.19 kB N/A -
turbopack-0u..8fol.js gzip 4.17 kB N/A -
turbopack-0x..iazv.js gzip 4.2 kB N/A -
turbopack-1x..5vxm.js gzip 4.19 kB N/A -
turbopack-2b..016f.js gzip 4.19 kB N/A -
turbopack-2h..-wl2.js gzip 4.19 kB N/A -
turbopack-2j..-y6j.js gzip 4.19 kB N/A -
turbopack-34..u2y9.js gzip 4.19 kB N/A -
turbopack-36..95b2.js gzip 4.19 kB N/A -
turbopack-3g..v35j.js gzip 4.19 kB N/A -
turbopack-3j..wswm.js gzip 4.19 kB N/A -
turbopack-3q..bf9c.js gzip 4.19 kB N/A -
00h4u194bb8c7.js gzip N/A 155 B -
06s2a1-sw8s8o.js gzip N/A 158 B -
0arkbdqpxc37i.js gzip N/A 8.6 kB -
0bz-xifewa17d.js gzip N/A 8.63 kB -
0em4a2sxerhz4.js gzip N/A 168 B -
0fbm505yboynb.js gzip N/A 49.3 kB -
0pmle13jdhdu0.js gzip N/A 65.5 kB -
0tvekitj587fh.js gzip N/A 8.51 kB -
0u3coesgskysq.js gzip N/A 161 B -
0yvk6-wi8e9wh.js gzip N/A 13.3 kB -
0z83a1om5rvtt.js gzip N/A 7.61 kB -
0zthaynjuxk58.js gzip N/A 157 B -
1-jqyfc89tixo.js gzip N/A 1.46 kB -
13q7l6-nygmmo.js gzip N/A 155 B -
14t1kneseb8th.js gzip N/A 2.3 kB -
15sb1-dsqfk_j.js gzip N/A 8.59 kB -
19mzuj3yh9is8.js gzip N/A 70.8 kB -
1ab2xruymo-oj.js gzip N/A 449 B -
1fx2d-glwbnlk.js gzip N/A 155 B -
1h55541zppmdw.js gzip N/A 156 B -
1tu25qtsmfhar.js gzip N/A 9.82 kB -
1vein_gnv3mwr.js gzip N/A 8.56 kB -
1wzrm0xjjbzn5.js gzip N/A 10.1 kB -
1z3g0uaqtv9_3.js gzip N/A 8.56 kB -
25a1yz7zua29z.js gzip N/A 13.8 kB -
2bi5hx402juv-.js gzip N/A 8.58 kB -
2hy56297fog9u.js gzip N/A 8.52 kB -
2u_rpxq3tzytl.js gzip N/A 233 B -
2wyls4kpqcn48.js gzip N/A 155 B -
3-a4k89t_92ej.js gzip N/A 153 B -
368lim5wq0o0r.js gzip N/A 12.9 kB -
3dj55jtmtuus3.js gzip N/A 155 B -
3drqjohogojbw.js gzip N/A 5.69 kB -
3g8l1m2-o-ewi.js gzip N/A 13.1 kB -
3jmkxsnxg0nrh.js gzip N/A 10.4 kB -
3v_4mi6gvc1jd.js gzip N/A 152 B -
3wpp8nvyoj121.js gzip N/A 9.24 kB -
42eqqiw5x99_v.js gzip N/A 158 B -
turbopack-0g..siep.js gzip N/A 4.19 kB -
turbopack-0n..58uq.js gzip N/A 4.19 kB -
turbopack-16..ev3s.js gzip N/A 4.19 kB -
turbopack-18..3_jf.js gzip N/A 4.19 kB -
turbopack-1d..i_fs.js gzip N/A 4.21 kB -
turbopack-1e..4m2e.js gzip N/A 4.19 kB -
turbopack-1e..tlsj.js gzip N/A 4.19 kB -
turbopack-1g..nrg9.js gzip N/A 4.19 kB -
turbopack-2a..b-mj.js gzip N/A 4.19 kB -
turbopack-2s.._bhv.js gzip N/A 4.19 kB -
turbopack-2y..bi9m.js gzip N/A 4.17 kB -
turbopack-32..dufm.js gzip N/A 4.19 kB -
turbopack-3b..a3aj.js gzip N/A 4.19 kB -
turbopack-3i..xnar.js gzip N/A 4.19 kB -
Total 465 kB 465 kB ⚠️ +12 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 719 B 718 B
Total 719 B 718 B ✅ -1 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 433 B 436 B
Total 433 B 436 B ⚠️ +3 B

📦 Webpack

Client

Main Bundles
Canary PR Change
2637-HASH.js gzip 4.63 kB N/A -
7724.HASH.js gzip 169 B N/A -
8274-HASH.js gzip 61.4 kB N/A -
8817-HASH.js gzip 5.59 kB N/A -
c3500254-HASH.js gzip 62.8 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 254 B 255 B
main-HASH.js gzip 39.4 kB 39.3 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
5887-HASH.js gzip N/A 5.61 kB -
6522-HASH.js gzip N/A 60.8 kB -
6779-HASH.js gzip N/A 4.63 kB -
8854.HASH.js gzip N/A 169 B -
eab920f9-HASH.js gzip N/A 62.8 kB -
Total 236 kB 235 kB ✅ -644 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 193 B 193 B
_error-HASH.js gzip 182 B 182 B
css-HASH.js gzip 333 B 334 B
dynamic-HASH.js gzip 1.81 kB 1.8 kB
edge-ssr-HASH.js gzip 255 B 255 B
head-HASH.js gzip 353 B 349 B 🟢 4 B (-1%)
hooks-HASH.js gzip 384 B 382 B
image-HASH.js gzip 581 B 581 B
index-HASH.js gzip 260 B 259 B
link-HASH.js gzip 2.51 kB 2.51 kB
routerDirect..HASH.js gzip 316 B 318 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 313 B 314 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.98 kB 7.97 kB ✅ -10 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 126 kB 126 kB
page.js gzip 273 kB 273 kB
Total 399 kB 399 kB ✅ -387 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 620 B 618 B
middleware-r..fest.js gzip 156 B 156 B
middleware.js gzip 44.4 kB 44.5 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 46 kB 46.1 kB ⚠️ +100 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 721 B 720 B
Total 721 B 720 B ✅ -1 B
Build Cache
Canary PR Change
0.pack gzip 4.38 MB 4.37 MB 🟢 4.62 kB (0%)
index.pack gzip 114 kB 112 kB 🟢 2.56 kB (-2%)
index.pack.old gzip 112 kB 113 kB 🔴 +1.27 kB (+1%)
Total 4.61 MB 4.6 MB ✅ -5.91 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 347 kB 347 kB
app-page-exp..prod.js gzip 192 kB 192 kB
app-page-tur...dev.js gzip 346 kB 346 kB
app-page-tur..prod.js gzip 192 kB 192 kB
app-page-tur...dev.js gzip 343 kB 343 kB
app-page-tur..prod.js gzip 190 kB 190 kB
app-page.run...dev.js gzip 343 kB 343 kB
app-page.run..prod.js gzip 190 kB 190 kB
app-route-ex...dev.js gzip 77 kB 77 kB
app-route-ex..prod.js gzip 52.5 kB 52.5 kB
app-route-tu...dev.js gzip 77.1 kB 77.1 kB
app-route-tu..prod.js gzip 52.6 kB 52.6 kB
app-route-tu...dev.js gzip 76.7 kB 76.7 kB
app-route-tu..prod.js gzip 52.3 kB 52.3 kB
app-route.ru...dev.js gzip 76.6 kB 76.6 kB
app-route.ru..prod.js gzip 52.3 kB 52.3 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.9 kB 43.9 kB
pages-api-tu..prod.js gzip 33.5 kB 33.5 kB
pages-api.ru...dev.js gzip 43.9 kB 43.9 kB
pages-api.ru..prod.js gzip 33.5 kB 33.5 kB
pages-turbo....dev.js gzip 53.3 kB 53.3 kB
pages-turbo...prod.js gzip 39.1 kB 39.1 kB
pages.runtim...dev.js gzip 53.3 kB 53.3 kB
pages.runtim..prod.js gzip 39.1 kB 39.1 kB
server.runti..prod.js gzip 63 kB 63 kB
Total 3.06 MB 3.06 MB ✅ -3 B
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/181e599e758e6cbee8a31fcbbe23c13465ee94b0/next

@sokra sokra requested a review from mmastrac April 17, 2026 13:00
@sokra sokra marked this pull request as ready for review April 17, 2026 13:00
const nativeBinaryDir = path.join(__dirname, '../../native')

// Sum the size of all *.node files in the action's native/ directory.
async function getSwcBinarySize() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be more valuable if we either break out per-platform or choose one/two to focus on (maybe x86_64 Linux + aarch64 darwin?). The total size is only somewhat meaningful.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only Linux, since we only build linux

return undefined
}

// Generate the dedicated Native Binary section shown after Bundle Sizes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably test this by adding something like this to the binary cdylib crate:

#[used]
#[unsafe(no_mangle)]
pub static  LARGE_ARRAY: [u8; 10_000_000] = [1; 10_000_000];

    // elsewhere...
    if std::hint::black_box(&LARGE_ARRAY).len() == 0 {
        eprintln!("{LARGE_ARRAY:?}");
    }

Copy link
Copy Markdown
Contributor

@mmastrac mmastrac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can land it and tweak.

if (!entry.endsWith('.node')) continue
const stat = await fs.stat(path.join(nativeBinaryDir, entry))
if (stat.isFile()) {
total += stat.size
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be good to measure the size when it is stripped and when it is gzipped as well

@sokra
Copy link
Copy Markdown
Member Author

sokra commented Apr 18, 2026

Landing, and look at your comments as follow-up

@sokra sokra merged commit bb2a445 into canary Apr 18, 2026
337 of 343 checks passed
@sokra sokra deleted the sokra/stats-binary-size branch April 18, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

created-by: Turbopack team PRs by the Turbopack team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants