[{"data":1,"prerenderedAt":1088},["ShallowReactive",2],{"content:\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Ftracking-bundle-size-per-pull-request":3,"surroundings:\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Ftracking-bundle-size-per-pull-request":1079},{"id":4,"title":5,"body":6,"description":1059,"extension":1060,"meta":1061,"navigation":750,"path":1073,"seo":1074,"stem":1077,"__hash__":1078},"content\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Ftracking-bundle-size-per-pull-request\u002Findex.md","Tracking Bundle Size per Pull Request",{"type":7,"value":8,"toc":1044},"minimark",[9,14,34,37,183,188,221,225,231,237,243,249,333,337,342,345,625,628,632,635,724,727,731,734,781,784,889,893,896,899,903,906,910,917,921,950,954,966,975,984,993,1002,1006,1029,1034,1037,1040],[10,11,13],"h1",{"id":12},"how-to-track-javascript-bundle-size-on-every-pull-request","How to Track JavaScript Bundle Size on Every Pull Request",[15,16,17,18,23,24,28,29,33],"p",{},"This guide is the visibility layer of ",[19,20,22],"a",{"href":21},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002F","JavaScript Performance Budgets",", part of ",[19,25,27],{"href":26},"\u002Fjavascript-bundle-optimization-code-splitting\u002F","JavaScript Bundle Optimization & Code Splitting",". Budgets stop large regressions; size tracking stops the small ones from accumulating unnoticed. A 6KB increase does not fail a budget, but twenty of them in a quarter add 120KB to the landing route. If every pull request shows its delta — \"+6.2KB to the landing route, from ",[30,31,32],"code",{},"react-hook-form","\" — reviewers can ask whether the cost is justified while the change is still easy to rework.",[15,35,36],{},"The mechanics are straightforward: build the base branch and the pull request, compute per-route sizes for both, and post the difference as a comment. Doing it well means comparing like with like (same build settings, same route definitions), attributing changes to modules so the comment explains itself, and storing results so trends are visible across months.",[15,38,39],{},[40,41,47,48,47,55,47,59,47,62,47,80,47,88,47,94,47,101,47,108,47,111,47,115,47,118,47,122,47,131,47,134,47,137,47,143,47,149,47,153,47,158,47,163,47,167,47,172,47,176,47,179,47],"svg",{"viewBox":42,"width":43,"role":44,"ariaLabel":45,"style":46},"0 0 760 330","100%","img","Sequence of CI building the base and head commits, computing route sizes and posting a diff comment on the pull request.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[49,50],"rect",{"className":51,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],"svg-canvas","0","#ffffff",[56,57,58],"title",{},"Per-PR size comparison",[60,61,45],"desc",{},[63,64,65],"defs",{},[66,67,74],"marker",{"id":68,"viewBox":69,"refX":70,"refY":71,"markerWidth":72,"markerHeight":72,"orient":73},"fa3f6bc920","0 0 10 10","9","5","7","auto-start-reverse",[75,76],"path",{"d":77,"fill":78,"style":79},"M0 0 L10 5 L0 10 z","currentColor","fill-opacity:0.7",[49,81],{"x":82,"y":82,"width":83,"height":84,"rx":85,"fill":86,"stroke":78,"style":87},"1","758","328","10","none","stroke-opacity:0.18",[89,90,58],"text",{"x":91,"y":92,"fill":78,"style":93},"28.0","34.0","font-size:16px;font-weight:700",[49,95],{"x":96,"y":97,"width":98,"height":92,"rx":99,"fill":78,"stroke":78,"style":100},"70.3","56.0","150.0","6","fill-opacity:0.06;stroke-opacity:0.4",[89,102,107],{"x":103,"y":104,"fill":78,"style":105,"textAnchor":106},"145.3","78.0","font-size:13px;font-weight:700","middle","Pull request",[49,109],{"x":110,"y":97,"width":98,"height":92,"rx":99,"fill":78,"stroke":78,"style":100},"305.0",[89,112,114],{"x":113,"y":104,"fill":78,"style":105,"textAnchor":106},"380.0","CI",[49,116],{"x":117,"y":97,"width":98,"height":92,"rx":99,"fill":78,"stroke":78,"style":100},"539.7",[89,119,121],{"x":120,"y":104,"fill":78,"style":105,"textAnchor":106},"614.7","Size store",[123,124],"line",{"x1":103,"y1":125,"x2":103,"y2":126,"stroke":78,"strokeWidth":127,"strokeDashArray":128,"style":130},"90.0","308.0","1.5",[129,129],"4","stroke-opacity:0.35",[123,132],{"x1":113,"y1":125,"x2":113,"y2":126,"stroke":78,"strokeWidth":127,"strokeDashArray":133,"style":130},[129,129],[123,135],{"x1":120,"y1":125,"x2":120,"y2":126,"stroke":78,"strokeWidth":127,"strokeDashArray":136,"style":130},[129,129],[123,138],{"x1":103,"y1":139,"x2":140,"y2":139,"stroke":78,"strokeWidth":127,"style":141,"markerEnd":142},"124.0","378.0","stroke-opacity:0.6","url(#fa3f6bc920)",[89,144,148],{"x":145,"y":146,"fill":78,"style":147,"textAnchor":106},"262.7","117.0","font-size:12px","push (head)",[123,150],{"x1":113,"y1":151,"x2":152,"y2":151,"stroke":78,"strokeWidth":127,"style":141,"markerEnd":142},"164.0","612.7",[89,154,157],{"x":155,"y":156,"fill":78,"style":147,"textAnchor":106},"497.3","157.0","fetch base sizes",[123,159],{"x1":120,"y1":160,"x2":161,"y2":160,"stroke":78,"strokeWidth":127,"strokeDashArray":162,"style":141,"markerEnd":142},"204.0","382.0",[71,129],[89,164,166],{"x":155,"y":165,"fill":78,"style":147,"textAnchor":106},"197.0","main @ merge-base",[123,168],{"x1":113,"y1":169,"x2":170,"y2":169,"stroke":78,"strokeWidth":127,"strokeDashArray":171,"style":141,"markerEnd":142},"244.0","147.3",[71,129],[89,173,175],{"x":145,"y":174,"fill":78,"style":147,"textAnchor":106},"237.0","comment: +6.2KB landing",[123,177],{"x1":113,"y1":178,"x2":152,"y2":178,"stroke":78,"strokeWidth":127,"style":141,"markerEnd":142},"284.0",[89,180,182],{"x":155,"y":181,"fill":78,"style":147,"textAnchor":106},"277.0","store head sizes",[184,185,187],"h2",{"id":186},"rapid-diagnosis","Rapid Diagnosis",[189,190,191,199,209,215],"ul",{},[192,193,194,198],"li",{},[195,196,197],"strong",{},"Can a reviewer see size impact today?"," If they need to check out the branch and run an analyser, nobody will.",[192,200,201,204,205,208],{},[195,202,203],{},"Are comparisons against the merge base?"," Comparing against the latest ",[30,206,207],{},"main"," mixes in other merged changes.",[192,210,211,214],{},[195,212,213],{},"Are deltas attributed?"," \"+18KB\" without saying which module or route invites shrugging.",[192,216,217,220],{},[195,218,219],{},"Is history kept?"," Without stored results, you cannot answer \"when did the landing route grow by 80KB?\".",[184,222,224],{"id":223},"root-cause-analysis","Root Cause Analysis",[15,226,227,230],{},[195,228,229],{},"1. Invisible cost in review."," Code review focuses on correctness and readability; bundle cost is not visible in a diff of source files.",[15,232,233,236],{},[195,234,235],{},"2. Noisy comparisons."," Building base and head with different dependency installs, environment variables or caches produces spurious differences that teach people to ignore the numbers.",[15,238,239,242],{},[195,240,241],{},"3. Unexplained deltas."," A total delta without module attribution forces the author to investigate, and most will not.",[15,244,245,248],{},[195,246,247],{},"4. No long-term record."," Each PR's numbers vanish after merge, so slow trends are invisible until field data shows the consequence.",[15,250,251],{},[40,252,47,255,47,258,47,261,47,263,47,266,47,268,47,275,47,283,47,288,47,292,47,299,47,303,47,307,47,311,47,315,47,319,47,323,47,327,47],{"viewBox":253,"width":43,"role":44,"ariaLabel":254,"style":46},"0 0 760 220","Bar chart attributing a quarter of landing-route JavaScript growth to individual merged pull requests, most of them small.",[49,256],{"className":257,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,259,260],{},"Where a quarter's growth came from (landing route)",[60,262,254],{},[49,264],{"x":82,"y":82,"width":83,"height":265,"rx":85,"fill":86,"stroke":78,"style":87},"218",[89,267,260],{"x":91,"y":92,"fill":78,"style":93},[89,269,274],{"x":270,"y":271,"fill":78,"style":272,"textAnchor":273},"171.0","70.0","font-size:13px","end","PR #1412 form library",[49,276],{"x":277,"y":97,"width":278,"height":279,"rx":280,"fill":281,"stroke":281,"style":282},"183.0","306.3","19","3","#0466c8","fill-opacity:0.55;stroke-opacity:0.9",[89,284,287],{"x":285,"y":271,"fill":78,"style":286},"495.3","font-size:12px;font-weight:600","24KB",[89,289,291],{"x":270,"y":290,"fill":78,"style":272,"textAnchor":273},"101.0","PR #1388 analytics SDK",[49,293],{"x":277,"y":294,"width":295,"height":279,"rx":280,"fill":296,"stroke":297,"style":298},"87.0","395.7","#ffc300","#b8860b","fill-opacity:0.7;stroke-opacity:0.9",[89,300,302],{"x":301,"y":290,"fill":78,"style":286},"584.7","31KB",[89,304,306],{"x":270,"y":305,"fill":78,"style":272,"textAnchor":273},"132.0","14 PRs under 5KB each",[49,308],{"x":277,"y":309,"width":310,"height":279,"rx":280,"fill":296,"stroke":297,"style":298},"118.0","485.0",[89,312,314],{"x":313,"y":305,"fill":78,"style":286},"674.0","38KB",[89,316,318],{"x":270,"y":317,"fill":78,"style":272,"textAnchor":273},"163.0","PR #1450 icon set",[49,320],{"x":277,"y":321,"width":322,"height":279,"rx":280,"fill":281,"stroke":281,"style":282},"149.0","153.2",[89,324,326],{"x":325,"y":317,"fill":78,"style":286},"342.1","12KB",[89,328,332],{"x":91,"y":329,"fill":330,"style":331},"198.0","#51617a","font-size:12.5px","No single change failed a budget; the per-PR comment is what made the 105KB total visible.",[184,334,336],{"id":335},"step-by-step-resolution","Step-by-Step Resolution",[338,339,341],"h3",{"id":340},"_1-produce-a-stable-size-report-from-the-build","1. Produce a stable size report from the build",[15,343,344],{},"Emit a JSON report per route with raw and brotli sizes, generated identically for any commit.",[346,347,352],"pre",{"className":348,"code":349,"language":350,"meta":351,"style":351},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F scripts\u002Fsize-report.mjs → dist\u002Fsize-report.json\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { brotliCompressSync } from 'node:zlib';\nimport routes from '..\u002Fdist\u002Froute-files.json' with { type: 'json' };\nconst report = {};\nfor (const [route, files] of Object.entries(routes)) {\n  let raw = 0, br = 0;\n  for (const f of files) { const b = readFileSync(f); raw += b.length; br += brotliCompressSync(b).length; }\n  report[route] = { raw, br };\n}\nwriteFileSync('dist\u002Fsize-report.json', JSON.stringify(report, null, 2));\n\u002F\u002F trade-off: brotli at default quality in Node is slower than gzip; on very\n\u002F\u002F large builds compute it only for initial routes, not every lazy chunk.\n","javascript","",[30,353,354,362,383,398,423,439,478,502,559,570,576,613,619],{"__ignoreMap":351},[355,356,358],"span",{"class":123,"line":357},1,[355,359,361],{"class":360},"sjfSM","\u002F\u002F scripts\u002Fsize-report.mjs → dist\u002Fsize-report.json\n",[355,363,365,369,373,376,380],{"class":123,"line":364},2,[355,366,368],{"class":367},"sPARh","import",[355,370,372],{"class":371},"saISM"," { readFileSync, writeFileSync } ",[355,374,375],{"class":367},"from",[355,377,379],{"class":378},"sZ8jY"," 'node:fs'",[355,381,382],{"class":371},";\n",[355,384,386,388,391,393,396],{"class":123,"line":385},3,[355,387,368],{"class":367},[355,389,390],{"class":371}," { brotliCompressSync } ",[355,392,375],{"class":367},[355,394,395],{"class":378}," 'node:zlib'",[355,397,382],{"class":371},[355,399,401,403,406,408,411,414,417,420],{"class":123,"line":400},4,[355,402,368],{"class":367},[355,404,405],{"class":371}," routes ",[355,407,375],{"class":367},[355,409,410],{"class":378}," '..\u002Fdist\u002Froute-files.json'",[355,412,413],{"class":367}," with",[355,415,416],{"class":371}," { type: ",[355,418,419],{"class":378},"'json'",[355,421,422],{"class":371}," };\n",[355,424,426,429,433,436],{"class":123,"line":425},5,[355,427,428],{"class":367},"const",[355,430,432],{"class":431},"sPXB4"," report",[355,434,435],{"class":367}," =",[355,437,438],{"class":371}," {};\n",[355,440,442,445,448,450,453,456,459,462,465,468,471,475],{"class":123,"line":441},6,[355,443,444],{"class":367},"for",[355,446,447],{"class":371}," (",[355,449,428],{"class":367},[355,451,452],{"class":371}," [",[355,454,455],{"class":431},"route",[355,457,458],{"class":371},", ",[355,460,461],{"class":431},"files",[355,463,464],{"class":371},"] ",[355,466,467],{"class":367},"of",[355,469,470],{"class":371}," Object.",[355,472,474],{"class":473},"smZ65","entries",[355,476,477],{"class":371},"(routes)) {\n",[355,479,481,484,487,490,493,496,498,500],{"class":123,"line":480},7,[355,482,483],{"class":367},"  let",[355,485,486],{"class":371}," raw ",[355,488,489],{"class":367},"=",[355,491,492],{"class":431}," 0",[355,494,495],{"class":371},", br ",[355,497,489],{"class":367},[355,499,492],{"class":431},[355,501,382],{"class":371},[355,503,505,508,510,512,515,518,521,523,526,528,531,534,537,540,543,546,548,551,554,556],{"class":123,"line":504},8,[355,506,507],{"class":367},"  for",[355,509,447],{"class":371},[355,511,428],{"class":367},[355,513,514],{"class":431}," f",[355,516,517],{"class":367}," of",[355,519,520],{"class":371}," files) { ",[355,522,428],{"class":367},[355,524,525],{"class":431}," b",[355,527,435],{"class":367},[355,529,530],{"class":473}," readFileSync",[355,532,533],{"class":371},"(f); raw ",[355,535,536],{"class":367},"+=",[355,538,539],{"class":371}," b.",[355,541,542],{"class":431},"length",[355,544,545],{"class":371},"; br ",[355,547,536],{"class":367},[355,549,550],{"class":473}," brotliCompressSync",[355,552,553],{"class":371},"(b).",[355,555,542],{"class":431},[355,557,558],{"class":371},"; }\n",[355,560,562,565,567],{"class":123,"line":561},9,[355,563,564],{"class":371},"  report[route] ",[355,566,489],{"class":367},[355,568,569],{"class":371}," { raw, br };\n",[355,571,573],{"class":123,"line":572},10,[355,574,575],{"class":371},"}\n",[355,577,579,582,585,588,590,593,596,599,602,605,607,610],{"class":123,"line":578},11,[355,580,581],{"class":473},"writeFileSync",[355,583,584],{"class":371},"(",[355,586,587],{"class":378},"'dist\u002Fsize-report.json'",[355,589,458],{"class":371},[355,591,592],{"class":431},"JSON",[355,594,595],{"class":371},".",[355,597,598],{"class":473},"stringify",[355,600,601],{"class":371},"(report, ",[355,603,604],{"class":431},"null",[355,606,458],{"class":371},[355,608,609],{"class":431},"2",[355,611,612],{"class":371},"));\n",[355,614,616],{"class":123,"line":615},12,[355,617,618],{"class":360},"\u002F\u002F trade-off: brotli at default quality in Node is slower than gzip; on very\n",[355,620,622],{"class":123,"line":621},13,[355,623,624],{"class":360},"\u002F\u002F large builds compute it only for initial routes, not every lazy chunk.\n",[15,626,627],{},"Expected outcome: a small JSON artefact that fully describes the build's route sizes.",[338,629,631],{"id":630},"_2-compare-against-the-merge-base-in-ci","2. Compare against the merge base in CI",[15,633,634],{},"Build the merge base in the same job (or fetch its stored report) and compute deltas.",[346,636,640],{"className":637,"code":638,"language":639,"meta":351,"style":351},"language-yaml shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# .github\u002Fworkflows\u002Fsize-diff.yml (excerpt)\n- uses: actions\u002Fcheckout@v4\n  with: { fetch-depth: 0 }\n- run: git checkout $(git merge-base origin\u002Fmain HEAD) && npm ci && npm run build:report && mv dist\u002Fsize-report.json \u002Ftmp\u002Fbase.json\n- run: git checkout ${{ github.sha }} && npm ci && npm run build:report\n- run: node scripts\u002Fsize-diff.mjs \u002Ftmp\u002Fbase.json dist\u002Fsize-report.json > size-comment.md\n# trade-off: building twice doubles CI time. Store each main-branch report as an\n# artefact on merge and download it here instead once the pipeline is stable.\n","yaml",[30,641,642,647,662,680,692,703,714,719],{"__ignoreMap":351},[355,643,644],{"class":123,"line":357},[355,645,646],{"class":360},"# .github\u002Fworkflows\u002Fsize-diff.yml (excerpt)\n",[355,648,649,652,656,659],{"class":123,"line":364},[355,650,651],{"class":371},"- ",[355,653,655],{"class":654},"sZBmE","uses",[355,657,658],{"class":371},": ",[355,660,661],{"class":378},"actions\u002Fcheckout@v4\n",[355,663,664,667,670,673,675,677],{"class":123,"line":385},[355,665,666],{"class":654},"  with",[355,668,669],{"class":371},": { ",[355,671,672],{"class":654},"fetch-depth",[355,674,658],{"class":371},[355,676,53],{"class":431},[355,678,679],{"class":371}," }\n",[355,681,682,684,687,689],{"class":123,"line":400},[355,683,651],{"class":371},[355,685,686],{"class":654},"run",[355,688,658],{"class":371},[355,690,691],{"class":378},"git checkout $(git merge-base origin\u002Fmain HEAD) && npm ci && npm run build:report && mv dist\u002Fsize-report.json \u002Ftmp\u002Fbase.json\n",[355,693,694,696,698,700],{"class":123,"line":425},[355,695,651],{"class":371},[355,697,686],{"class":654},[355,699,658],{"class":371},[355,701,702],{"class":378},"git checkout ${{ github.sha }} && npm ci && npm run build:report\n",[355,704,705,707,709,711],{"class":123,"line":441},[355,706,651],{"class":371},[355,708,686],{"class":654},[355,710,658],{"class":371},[355,712,713],{"class":378},"node scripts\u002Fsize-diff.mjs \u002Ftmp\u002Fbase.json dist\u002Fsize-report.json > size-comment.md\n",[355,715,716],{"class":123,"line":480},[355,717,718],{"class":360},"# trade-off: building twice doubles CI time. Store each main-branch report as an\n",[355,720,721],{"class":123,"line":504},[355,722,723],{"class":360},"# artefact on merge and download it here instead once the pipeline is stable.\n",[15,725,726],{},"Expected outcome: deltas that reflect only this pull request's changes.",[338,728,730],{"id":729},"_3-post-a-readable-comment-with-attribution","3. Post a readable comment with attribution",[15,732,733],{},"Format the diff as a short table: route, base, head, delta, and the top modules that changed (from the visualiser's raw data or source-map-explorer output). Update the same comment on each push rather than adding new ones.",[346,735,739],{"className":736,"code":737,"language":738,"meta":351,"style":351},"language-markdown shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","**Bundle size** (raw \u002F brotli, vs merge base)\n\n| Route | Base | Head | Δ |\n| --- | --- | --- | --- |\n| landing | 298.1 \u002F 92.4 KB | 304.3 \u002F 94.1 KB | **+6.2 \u002F +1.7 KB** |\n| checkout | 131.0 \u002F 41.2 KB | 131.0 \u002F 41.2 KB | 0 |\n\nTop change: `react-hook-form` +5.9 KB (landing)\n","markdown",[30,740,741,746,752,757,762,767,772,776],{"__ignoreMap":351},[355,742,743],{"class":123,"line":357},[355,744,745],{},"**Bundle size** (raw \u002F brotli, vs merge base)\n",[355,747,748],{"class":123,"line":364},[355,749,751],{"emptyLinePlaceholder":750},true,"\n",[355,753,754],{"class":123,"line":385},[355,755,756],{},"| Route | Base | Head | Δ |\n",[355,758,759],{"class":123,"line":400},[355,760,761],{},"| --- | --- | --- | --- |\n",[355,763,764],{"class":123,"line":425},[355,765,766],{},"| landing | 298.1 \u002F 92.4 KB | 304.3 \u002F 94.1 KB | **+6.2 \u002F +1.7 KB** |\n",[355,768,769],{"class":123,"line":441},[355,770,771],{},"| checkout | 131.0 \u002F 41.2 KB | 131.0 \u002F 41.2 KB | 0 |\n",[355,773,774],{"class":123,"line":480},[355,775,751],{"emptyLinePlaceholder":750},[355,777,778],{"class":123,"line":504},[355,779,780],{},"Top change: `react-hook-form` +5.9 KB (landing)\n",[15,782,783],{},"Expected outcome: reviewers see the cost and its cause at a glance.",[15,785,786],{},[40,787,47,790,47,793,47,796,47,798,47,801,47,803,47,809,47,815,47,819,47,822,47,826,47,829,47,832,47,836,47,840,47,843,47,847,47,851,47,857,47,861,47,865,47,870,47,872,47,875,47,877,47,880,47,883,47,886,47],{"viewBox":788,"width":43,"role":44,"ariaLabel":789,"style":46},"0 0 760 318","Four elements that make a per-pull-request bundle size comment actionable for authors and reviewers.",[49,791],{"className":792,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,794,795],{},"Anatomy of a useful size comment",[60,797,789],{},[49,799],{"x":82,"y":82,"width":83,"height":800,"rx":85,"fill":86,"stroke":78,"style":87},"316",[89,802,795],{"x":91,"y":92,"fill":78,"style":93},[49,804],{"x":805,"y":97,"width":806,"height":807,"rx":99,"fill":281,"stroke":281,"style":808},"72.0","660.0","51.0","fill-opacity:0.14;stroke-opacity:0.9",[89,810,814],{"x":811,"y":812,"fill":78,"style":105,"textAnchor":813},"86.0","77.0","start","Per-route totals",[89,816,818],{"x":811,"y":817,"fill":78,"style":147,"textAnchor":813},"94.0","Initial routes first; lazy routes collapsed",[49,820],{"x":805,"y":821,"width":806,"height":807,"rx":99,"fill":281,"stroke":281,"style":808},"119.0",[89,823,825],{"x":811,"y":824,"fill":78,"style":105,"textAnchor":813},"140.0","Raw and brotli deltas",[89,827,828],{"x":811,"y":156,"fill":78,"style":147,"textAnchor":813},"Parse cost and transfer cost side by side",[49,830],{"x":805,"y":831,"width":806,"height":807,"rx":99,"fill":281,"stroke":281,"style":808},"182.0",[89,833,835],{"x":811,"y":834,"fill":78,"style":105,"textAnchor":813},"203.0","Top changed modules",[89,837,839],{"x":811,"y":838,"fill":78,"style":147,"textAnchor":813},"220.0","The import that caused the change",[49,841],{"x":805,"y":842,"width":806,"height":807,"rx":99,"fill":281,"stroke":281,"style":808},"245.0",[89,844,846],{"x":811,"y":845,"fill":78,"style":105,"textAnchor":813},"266.0","Budget headroom",[89,848,850],{"x":811,"y":849,"fill":78,"style":147,"textAnchor":813},"283.0","How close each route is to its limit",[123,852],{"x1":853,"y1":854,"x2":853,"y2":855,"stroke":78,"strokeWidth":127,"style":856},"43.0","95.5","130.5","stroke-opacity:0.3",[123,858],{"x1":853,"y1":859,"x2":853,"y2":860,"stroke":78,"strokeWidth":127,"style":856},"158.5","193.5",[123,862],{"x1":853,"y1":863,"x2":853,"y2":864,"stroke":78,"strokeWidth":127,"style":856},"221.5","256.5",[866,867],"circle",{"cx":853,"cy":868,"r":869,"fill":281},"81.5","13",[89,871,82],{"x":853,"y":811,"fill":54,"style":105,"textAnchor":106},[866,873],{"cx":853,"cy":874,"r":869,"fill":281},"144.5",[89,876,609],{"x":853,"y":321,"fill":54,"style":105,"textAnchor":106},[866,878],{"cx":853,"cy":879,"r":869,"fill":281},"207.5",[89,881,280],{"x":853,"y":882,"fill":54,"style":105,"textAnchor":106},"212.0",[866,884],{"cx":853,"cy":885,"r":869,"fill":281},"270.5",[89,887,129],{"x":853,"y":888,"fill":54,"style":105,"textAnchor":106},"275.0",[338,890,892],{"id":891},"_4-store-every-main-branch-report","4. Store every main-branch report",[15,894,895],{},"Append each merge's report to a time series (a JSON file in a storage bucket, a database, or a service such as RelativeCI or bundlewatch). Plot initial-route sizes weekly.",[15,897,898],{},"Expected outcome: you can answer \"when did this route grow, and which PR did it?\" in minutes.",[184,900,902],{"id":901},"verification","Verification",[15,904,905],{},"Open a test pull request that adds a known dependency to the landing route and check that the comment reports a delta matching the dependency's size and names it. Open another that changes only lazy-route code and confirm the landing delta is zero. Review the stored history after a month: the series should be smooth, with steps that correspond to identifiable pull requests.",[184,907,909],{"id":908},"worked-example-making-growth-a-review-topic","Worked Example: Making Growth a Review Topic",[15,911,912,913,916],{},"A product team introduced per-PR size comments without changing any budgets. In the first month, reviewers questioned four changes: a carousel library added for one marketing page (moved behind a dynamic import, saving 41KB on all other pages), a duplicated copy of ",[30,914,915],{},"lodash"," introduced by a dependency upgrade (deduplicated, 24KB), an icon component importing a full icon set (switched to per-icon imports, 18KB), and an analytics SDK added to the entry (deferred to idle, removed 31KB from initial load). None of these would have failed the existing budgets. Visibility alone recovered more than 100KB from the landing route within weeks.",[184,918,920],{"id":919},"common-mistakes","Common Mistakes",[189,922,923,929,938,944],{},[192,924,925,928],{},[195,926,927],{},"Reporting only totals across all chunks."," Lazy route growth then masks initial-route growth and vice versa.",[192,930,931,937],{},[195,932,933,934,936],{},"Comparing against ",[30,935,207],{}," head instead of the merge base."," Other merged PRs appear as this PR's changes.",[192,939,940,943],{},[195,941,942],{},"Rounding to whole kilobytes."," Small deltas disappear, which is precisely what this process exists to show.",[192,945,946,949],{},[195,947,948],{},"Posting a new comment on every push."," The PR fills with stale comments; update one in place.",[184,951,953],{"id":952},"faq","FAQ",[955,956,959,963],"details",{"className":957},[958],"faq-item",[960,961,962],"summary",{},"Are hosted services worth it compared with a script?",[15,964,965],{},"Services like RelativeCI, bundlewatch or Codecov's bundle analysis provide history, attribution and comments out of the box, which is worth it for teams without time to maintain scripts. A script is enough for a single app and gives full control over route definitions; the important part is that comments appear on every PR.",[955,967,969,972],{"className":968},[958],[960,970,971],{},"Should size comments block merges?",[15,973,974],{},"No — that is the budget's job. Comments are informational; they make costs visible and start conversations. Combining both works well: a budget for hard limits, a comment for everything below them.",[955,976,978,981],{"className":977},[958],[960,979,980],{},"How do I attribute changes to modules cheaply?",[15,982,983],{},"Generate the visualiser's raw data or a source-map-explorer JSON for both builds and diff module sizes per chunk. Show only the top three or four changes; a full module diff is too noisy for a comment and belongs in a linked artefact.",[955,985,987,990],{"className":986},[958],[960,988,989],{},"Should lazy route changes appear in the comment?",[15,991,992],{},"Yes, but collapsed below initial routes. Lazy chunks matter for navigation speed, and a large jump in one still deserves a look; listing them after the initial routes keeps the critical-path numbers prominent without hiding the rest.",[955,994,996,999],{"className":995},[958],[960,997,998],{},"How do I handle pull requests that legitimately add weight?",[15,1000,1001],{},"Let them through with context. The comment's job is to make the cost visible, not to prevent it; a reviewer who sees \"+24KB for the new checkout validation\" can confirm it is needed on that route and lazy elsewhere. Record significant intentional increases in a short changelog so later investigations of growth find the reasoning immediately.",[184,1003,1005],{"id":1004},"related","Related",[189,1007,1008,1015,1022],{},[192,1009,1010,1014],{},[19,1011,1013],{"href":1012},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Fenforcing-bundle-budgets-with-size-limit\u002F","Enforcing bundle budgets with size-limit"," — the hard limits behind the comments.",[192,1016,1017,1021],{},[19,1018,1020],{"href":1019},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fwebpack-bundle-analysis-techniques\u002Fcomparing-two-builds-with-statoscope\u002F","Comparing two builds with Statoscope"," — deeper build-to-build comparison for webpack.",[192,1023,1024,1028],{},[19,1025,1027],{"href":1026},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fvite-and-rollup-build-optimization\u002Fanalyzing-vite-bundles-with-rollup-plugin-visualizer\u002F","Analyzing Vite bundles with rollup-plugin-visualizer"," — module-level data for attribution.",[1030,1031,1033],"script",{"type":1032},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Track JavaScript Bundle Size on Every Pull Request\",\n  \"description\": \"Make the JavaScript cost of every change visible in code review with base-branch comparisons, module-level attribution and a size history.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Produce a stable size report from the build\",\n      \"text\": \"Emit a JSON report per route with raw and brotli sizes, generated identically for any commit.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Compare against the merge base in CI\",\n      \"text\": \"Build the merge base in the same job (or fetch its stored report) and compute deltas.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Post a readable comment with attribution\",\n      \"text\": \"Format the diff as a short table: route, base, head, delta, and the top modules that changed (from the visualiser's raw data or source-map-explorer output).\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Store every main-branch report\",\n      \"text\": \"Append each merge's report to a time series (a JSON file in a storage bucket, a database, or a service such as RelativeCI or bundlewatch).\"\n    }\n  ]\n}\n",[1030,1035,1036],{"type":1032},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Track JavaScript Bundle Size on Every Pull Request\",\n  \"description\": \"Make the JavaScript cost of every change visible in code review with base-branch comparisons, module-level attribution and a size history.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Ftracking-bundle-size-per-pull-request\u002F\"\n  }\n}\n",[1030,1038,1039],{"type":1032},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"JavaScript Bundle Optimization & Code Splitting\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"JavaScript Performance Budgets\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Tracking Bundle Size per Pull Request\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Ftracking-bundle-size-per-pull-request\u002F\"\n    }\n  ]\n}\n",[1041,1042,1043],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--shiki-light:#024C1A}",{"title":351,"searchDepth":364,"depth":364,"links":1045},[1046,1047,1048,1054,1055,1056,1057,1058],{"id":186,"depth":364,"text":187},{"id":223,"depth":364,"text":224},{"id":335,"depth":364,"text":336,"children":1049},[1050,1051,1052,1053],{"id":340,"depth":385,"text":341},{"id":630,"depth":385,"text":631},{"id":729,"depth":385,"text":730},{"id":891,"depth":385,"text":892},{"id":901,"depth":364,"text":902},{"id":908,"depth":364,"text":909},{"id":919,"depth":364,"text":920},{"id":952,"depth":364,"text":953},{"id":1004,"depth":364,"text":1005},"Make the JavaScript cost of every change visible in code review with base-branch comparisons, module-level attribution and a size history.","md",{"slug":1062,"type":1063,"breadcrumb":1064,"datePublished":1072,"dateModified":1072},"tracking-bundle-size-per-pull-request","article",[1065,1068,1069,1070],{"name":1066,"url":1067},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1071},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Ftracking-bundle-size-per-pull-request\u002F","2026-10-06","\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Ftracking-bundle-size-per-pull-request",{"title":1075,"description":1076},"Tracking JavaScript Bundle Size per Pull Request","Post a per-route bundle size diff on every pull request, compare against the base branch, attribute changes to modules, and keep a history of sizes over time.","javascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Ftracking-bundle-size-per-pull-request\u002Findex","NjrDgZQLL6BUsku4-K1Xxrd_TktRbKYL-uvlxrUUTKg",[1080,1084],{"title":1081,"path":1082,"stem":1083},"Measuring JavaScript Parse and Compile Cost","\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Fmeasuring-javascript-parse-and-compile-cost","javascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Fmeasuring-javascript-parse-and-compile-cost\u002Findex",{"title":1085,"path":1086,"stem":1087},"Why Compressed Size Is the Wrong Budget","\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Fwhy-compressed-size-is-the-wrong-budget","javascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Fwhy-compressed-size-is-the-wrong-budget\u002Findex",1791308079012]