[{"data":1,"prerenderedAt":1222},["ShallowReactive",2],{"content:\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fprecompressing-static-assets-at-build-time":3,"surroundings:\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fprecompressing-static-assets-at-build-time":1214},{"id":4,"title":5,"body":6,"description":1194,"extension":1195,"meta":1196,"navigation":1207,"path":1208,"seo":1209,"stem":1212,"__hash__":1213},"content\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fprecompressing-static-assets-at-build-time\u002Findex.md","Precompressing Static Assets at Build Time",{"type":7,"value":8,"toc":1176},"minimark",[9,14,29,61,161,166,211,215,221,234,240,249,253,258,261,480,484,555,562,566,580,584,587,705,709,778,782,790,918,922,928,932,951,955,984,988,1002,1008,1014,1020,1024,1036,1045,1054,1063,1072,1081,1094,1107,1116,1125,1134,1138,1161,1166,1169,1172],[10,11,13],"h1",{"id":12},"how-to-precompress-static-assets-at-build-time","How to Precompress Static Assets at Build Time",[15,16,17,18,23,24,28],"p",{},"This guide is part of ",[19,20,22],"a",{"href":21},"\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002F","Compression: Brotli & Zstandard",", within ",[19,25,27],{"href":26},"\u002Fnetwork-protocol-optimization\u002F","Network & Server Response Optimization",". Static assets — hashed JavaScript and CSS bundles, SVGs, static HTML from site generators, JSON data files — are identical for every request until the next build. Compressing them on every request wastes CPU and forces a low compression level to keep latency acceptable. Compressing them once at build time lets you use the slowest, strongest settings: Brotli level 11 and gzip level 9 (or Zopfli), typically 5–10% smaller than on-the-fly levels, at zero runtime cost.",[15,30,31,32,36,37,40,41,44,45,48,49,52,53,56,57,60],{},"The build produces ",[33,34,35],"code",{},"app.js",", ",[33,38,39],{},"app.js.br"," and ",[33,42,43],{},"app.js.gz"," side by side. The server (or CDN) inspects ",[33,46,47],{},"Accept-Encoding"," and serves the matching precompressed file with the right ",[33,50,51],{},"Content-Encoding"," header. The pieces that go wrong are serving configuration (files exist but are not used), wrong headers (",[33,54,55],{},"Content-Type"," set to ",[33,58,59],{},"application\u002Fx-brotli","), and stale compressed files after a build change.",[15,62,63],{},[64,65,71,72,71,79,71,83,71,86,71,95,71,101,71,108,71,117,71,122,71,126,71,130,71,134,71,138,71,142,71,146,71,150,71,157,71],"svg",{"viewBox":66,"width":67,"role":68,"ariaLabel":69,"style":70},"0 0 760 194","100%","img","Bar chart comparing the size of one JavaScript bundle compressed on the fly versus precompressed at maximum levels.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[73,74],"rect",{"className":75,"x":77,"y":77,"width":67,"height":67,"fill":78},[76],"svg-canvas","0","#ffffff",[80,81,82],"title",{},"Main bundle size by compression approach (612KB raw)",[84,85,69],"desc",{},[73,87],{"x":88,"y":88,"width":89,"height":90,"rx":91,"fill":92,"stroke":93,"style":94},"1","758","192","10","none","currentColor","stroke-opacity:0.18",[96,97,82],"text",{"x":98,"y":99,"fill":93,"style":100},"28.0","34.0","font-size:16px;font-weight:700",[96,102,107],{"x":103,"y":104,"fill":93,"style":105,"textAnchor":106},"199.3","70.0","font-size:13px","end","gzip 6 on the fly",[73,109],{"x":110,"y":111,"width":112,"height":113,"rx":114,"fill":115,"stroke":115,"style":116},"211.3","56.0","456.7","19","3","#0466c8","fill-opacity:0.55;stroke-opacity:0.9",[96,118,121],{"x":119,"y":104,"fill":93,"style":120},"674.0","font-size:12px;font-weight:600","178KB",[96,123,125],{"x":103,"y":124,"fill":93,"style":105,"textAnchor":106},"101.0","gzip 9 \u002F Zopfli precompressed",[73,127],{"x":110,"y":128,"width":129,"height":113,"rx":114,"fill":115,"stroke":115,"style":116},"87.0","438.7",[96,131,133],{"x":132,"y":124,"fill":93,"style":120},"656.0","171KB",[96,135,137],{"x":103,"y":136,"fill":93,"style":105,"textAnchor":106},"132.0","Brotli 5 on the fly",[73,139],{"x":110,"y":140,"width":141,"height":113,"rx":114,"fill":115,"stroke":115,"style":116},"118.0","418.2",[96,143,145],{"x":144,"y":136,"fill":93,"style":120},"635.5","163KB",[96,147,149],{"x":103,"y":148,"fill":93,"style":105,"textAnchor":106},"163.0","Brotli 11 precompressed",[73,151],{"x":110,"y":152,"width":153,"height":113,"rx":114,"fill":154,"stroke":155,"style":156},"149.0","379.7","#ffc300","#b8860b","fill-opacity:0.7;stroke-opacity:0.9",[96,158,160],{"x":159,"y":148,"fill":93,"style":120},"597.0","148KB",[162,163,165],"h2",{"id":164},"rapid-diagnosis","Rapid Diagnosis",[167,168,169,185,199,205],"ul",{},[170,171,172,176,177,180,181,184],"li",{},[173,174,175],"strong",{},"Check build output"," for ",[33,178,179],{},".br","\u002F",[33,182,183],{},".gz"," files next to assets.",[170,186,187,190,191,194,195,198],{},[173,188,189],{},"Check response headers",": ",[33,192,193],{},"content-encoding: br"," and correct ",[33,196,197],{},"content-type"," for JS and CSS.",[170,200,201,204],{},[173,202,203],{},"Compare transfer size"," with a locally computed Brotli 11 size; a larger transfer means on-the-fly compression at a lower level.",[170,206,207,210],{},[173,208,209],{},"Check server CPU"," for compression time on static routes.",[162,212,214],{"id":213},"root-cause-analysis","Root Cause Analysis",[15,216,217,220],{},[173,218,219],{},"1. No precompression step."," Assets are compressed per request by the server or CDN.",[15,222,223,226,227,180,230,233],{},[173,224,225],{},"2. Precompressed files ignored."," Server lacks ",[33,228,229],{},"brotli_static",[33,231,232],{},"gzip_static"," or equivalent.",[15,235,236,239],{},[173,237,238],{},"3. CDN recompresses."," The CDN compresses at the edge at its own level instead of using origin files.",[15,241,242,245,246,248],{},[173,243,244],{},"4. Wrong headers."," Compressed files served with incorrect ",[33,247,55],{}," break loading.",[162,250,252],{"id":251},"step-by-step-resolution","Step-by-Step Resolution",[254,255,257],"h3",{"id":256},"_1-add-a-precompression-step","1. Add a precompression step",[15,259,260],{},"Use a bundler plugin (for example vite-plugin-compression, compression-webpack-plugin) or a small script after the build.",[262,263,268],"pre",{"className":264,"code":265,"language":266,"meta":267,"style":267},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F vite.config.js\nimport compression from 'vite-plugin-compression2';\nexport default {\n  plugins: [\n    compression({ algorithm: 'brotliCompress', include: \u002F\\.(js|css|html|svg|json|xml|txt|wasm)$\u002F, threshold: 1024 }),\n    compression({ algorithm: 'gzip', include: \u002F\\.(js|css|html|svg|json|xml|txt|wasm)$\u002F, threshold: 1024 }),\n  ],\n};\n\u002F\u002F trade-off: maximum Brotli adds build time (seconds per large bundle); cache\n\u002F\u002F build outputs so unchanged hashed files are not recompressed.\n","javascript","",[33,269,270,279,300,313,319,395,451,460,468,474],{"__ignoreMap":267},[271,272,275],"span",{"class":273,"line":274},"line",1,[271,276,278],{"class":277},"sjfSM","\u002F\u002F vite.config.js\n",[271,280,282,286,290,293,297],{"class":273,"line":281},2,[271,283,285],{"class":284},"sPARh","import",[271,287,289],{"class":288},"saISM"," compression ",[271,291,292],{"class":284},"from",[271,294,296],{"class":295},"sZ8jY"," 'vite-plugin-compression2'",[271,298,299],{"class":288},";\n",[271,301,303,306,309],{"class":273,"line":302},3,[271,304,305],{"class":284},"export",[271,307,308],{"class":284}," default",[271,310,312],{"class":311},"sQw3B"," {\n",[271,314,316],{"class":273,"line":315},4,[271,317,318],{"class":288},"  plugins: [\n",[271,320,322,326,329,332,335,338,342,345,348,351,353,356,358,360,362,365,367,370,372,375,377,380,383,385,388,392],{"class":273,"line":321},5,[271,323,325],{"class":324},"smZ65","    compression",[271,327,328],{"class":288},"({ algorithm: ",[271,330,331],{"class":295},"'brotliCompress'",[271,333,334],{"class":288},", include:",[271,336,337],{"class":295}," \u002F",[271,339,341],{"class":340},"sn2CQ","\\.",[271,343,344],{"class":295},"(js",[271,346,347],{"class":284},"|",[271,349,350],{"class":295},"css",[271,352,347],{"class":284},[271,354,355],{"class":295},"html",[271,357,347],{"class":284},[271,359,64],{"class":295},[271,361,347],{"class":284},[271,363,364],{"class":295},"json",[271,366,347],{"class":284},[271,368,369],{"class":295},"xml",[271,371,347],{"class":284},[271,373,374],{"class":295},"txt",[271,376,347],{"class":284},[271,378,379],{"class":295},"wasm)",[271,381,382],{"class":284},"$",[271,384,180],{"class":295},[271,386,387],{"class":288},", threshold: ",[271,389,391],{"class":390},"sPXB4","1024",[271,393,394],{"class":288}," }),\n",[271,396,398,400,402,405,407,409,411,413,415,417,419,421,423,425,427,429,431,433,435,437,439,441,443,445,447,449],{"class":273,"line":397},6,[271,399,325],{"class":324},[271,401,328],{"class":288},[271,403,404],{"class":295},"'gzip'",[271,406,334],{"class":288},[271,408,337],{"class":295},[271,410,341],{"class":340},[271,412,344],{"class":295},[271,414,347],{"class":284},[271,416,350],{"class":295},[271,418,347],{"class":284},[271,420,355],{"class":295},[271,422,347],{"class":284},[271,424,64],{"class":295},[271,426,347],{"class":284},[271,428,364],{"class":295},[271,430,347],{"class":284},[271,432,369],{"class":295},[271,434,347],{"class":284},[271,436,374],{"class":295},[271,438,347],{"class":284},[271,440,379],{"class":295},[271,442,382],{"class":284},[271,444,180],{"class":295},[271,446,387],{"class":288},[271,448,391],{"class":390},[271,450,394],{"class":288},[271,452,454,457],{"class":273,"line":453},7,[271,455,456],{"class":288},"  ]",[271,458,459],{"class":311},",\n",[271,461,463,466],{"class":273,"line":462},8,[271,464,465],{"class":311},"}",[271,467,299],{"class":288},[271,469,471],{"class":273,"line":470},9,[271,472,473],{"class":277},"\u002F\u002F trade-off: maximum Brotli adds build time (seconds per large bundle); cache\n",[271,475,477],{"class":273,"line":476},10,[271,478,479],{"class":277},"\u002F\u002F build outputs so unchanged hashed files are not recompressed.\n",[254,481,483],{"id":482},"_2-serve-precompressed-files","2. Serve precompressed files",[262,485,489],{"className":486,"code":487,"language":488,"meta":267,"style":267},"language-nginx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","location \u002Fassets\u002F {\n  brotli_static on;          # serve .br when Accept-Encoding includes br\n  gzip_static on;            # serve .gz otherwise\n  add_header Cache-Control \"public, max-age=31536000, immutable\";\n  add_header Vary Accept-Encoding;\n}\n","nginx",[33,490,491,502,516,530,543,550],{"__ignoreMap":267},[271,492,493,496,499],{"class":273,"line":274},[271,494,495],{"class":284},"location",[271,497,498],{"class":311}," \u002Fassets\u002F ",[271,500,501],{"class":288},"{\n",[271,503,504,507,510,513],{"class":273,"line":281},[271,505,506],{"class":284},"  brotli_static",[271,508,509],{"class":390}," on",[271,511,512],{"class":288},";          ",[271,514,515],{"class":277},"# serve .br when Accept-Encoding includes br\n",[271,517,518,521,524,527],{"class":273,"line":302},[271,519,520],{"class":284},"  gzip_static ",[271,522,523],{"class":390},"on",[271,525,526],{"class":288},";            ",[271,528,529],{"class":277},"# serve .gz otherwise\n",[271,531,532,535,538,541],{"class":273,"line":315},[271,533,534],{"class":284},"  add_header ",[271,536,537],{"class":288},"Cache-Control ",[271,539,540],{"class":295},"\"public, max-age=31536000, immutable\"",[271,542,299],{"class":288},[271,544,545,547],{"class":273,"line":321},[271,546,534],{"class":284},[271,548,549],{"class":288},"Vary Accept-Encoding;\n",[271,551,552],{"class":273,"line":397},[271,553,554],{"class":288},"}\n",[15,556,557,558,561],{},"For Node.js, ",[33,559,560],{},"express-static-gzip"," or similar middleware serves precompressed variants. Static hosts (Netlify, Cloudflare Pages, Vercel) compress automatically; check whether they use your precompressed files or compress at the edge.",[254,563,565],{"id":564},"_3-upload-to-object-storage-correctly","3. Upload to object storage correctly",[15,567,568,569,572,573,575,576,579],{},"When serving from S3-like storage behind a CDN, upload the compressed body under the original name with ",[33,570,571],{},"Content-Encoding: br",", or use the CDN's encoding-aware variants. Never serve ",[33,574,179],{}," files with ",[33,577,578],{},"Content-Type: application\u002Fx-brotli",".",[254,581,583],{"id":582},"_4-exclude-already-compressed-files","4. Exclude already-compressed files",[15,585,586],{},"Skip images, fonts (WOFF2), video, audio and archives.",[15,588,589],{},[64,590,71,593,71,596,71,599,71,601,71,618,71,621,71,623,71,629,71,636,71,641,71,645,71,649,71,652,71,655,71,659,71,662,71,665,71,669,71,672,71,675,71,678,71,682,71,685,71,693,71,697,71,701,71],{"viewBox":591,"width":67,"role":68,"ariaLabel":592,"style":70},"0 0 760 149","Pipeline from bundling hashed assets to precompressing and serving the right variant per request.",[73,594],{"className":595,"x":77,"y":77,"width":67,"height":67,"fill":78},[76],[80,597,598],{},"Build-time compression pipeline",[84,600,592],{},[602,603,604],"defs",{},[605,606,613],"marker",{"id":607,"viewBox":608,"refX":609,"refY":610,"markerWidth":611,"markerHeight":611,"orient":612},"fa97d91348","0 0 10 10","9","5","7","auto-start-reverse",[614,615],"path",{"d":616,"fill":93,"style":617},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[73,619],{"x":88,"y":88,"width":89,"height":620,"rx":91,"fill":92,"stroke":93,"style":94},"147",[96,622,598],{"x":98,"y":99,"fill":93,"style":100},[73,624],{"x":98,"y":111,"width":625,"height":626,"rx":627,"fill":115,"stroke":115,"style":628},"108.8","71.0","6","fill-opacity:0.14;stroke-opacity:0.9",[96,630,635],{"x":631,"y":632,"fill":93,"style":633,"textAnchor":634},"82.4","78.0","font-size:13px;font-weight:700","middle","Bundle",[96,637,640],{"x":631,"y":638,"fill":93,"style":639,"textAnchor":634},"96.0","font-size:12px","hashed outputs",[73,642],{"x":643,"y":111,"width":625,"height":626,"rx":627,"fill":154,"stroke":155,"style":644},"176.8","fill-opacity:0.24;stroke-opacity:0.9",[96,646,648],{"x":647,"y":632,"fill":93,"style":633,"textAnchor":634},"231.2","Compress",[96,650,651],{"x":647,"y":638,"fill":93,"style":639,"textAnchor":634},"br 11 + gz 9",[73,653],{"x":654,"y":111,"width":625,"height":626,"rx":627,"fill":115,"stroke":115,"style":628},"325.6",[96,656,658],{"x":657,"y":632,"fill":93,"style":633,"textAnchor":634},"380.0","Deploy",[96,660,661],{"x":657,"y":638,"fill":93,"style":639,"textAnchor":634},"files side by side",[73,663],{"x":664,"y":111,"width":625,"height":626,"rx":627,"fill":115,"stroke":115,"style":628},"474.4",[96,666,668],{"x":667,"y":632,"fill":93,"style":633,"textAnchor":634},"528.8","Serve",[96,670,671],{"x":667,"y":638,"fill":93,"style":639,"textAnchor":634},"pick by",[96,673,47],{"x":667,"y":674,"fill":93,"style":639,"textAnchor":634},"112.0",[73,676],{"x":677,"y":111,"width":625,"height":626,"rx":627,"fill":115,"stroke":115,"style":628},"623.2",[96,679,681],{"x":680,"y":632,"fill":93,"style":633,"textAnchor":634},"677.6","Cache",[96,683,684],{"x":680,"y":638,"fill":93,"style":639,"textAnchor":634},"immutable + Vary",[273,686],{"x1":687,"y1":688,"x2":689,"y2":688,"stroke":93,"strokeWidth":690,"style":691,"markerEnd":692},"136.8","91.5","174.8","1.5","stroke-opacity:0.6","url(#fa97d91348)",[273,694],{"x1":695,"y1":688,"x2":696,"y2":688,"stroke":93,"strokeWidth":690,"style":691,"markerEnd":692},"285.6","323.6",[273,698],{"x1":699,"y1":688,"x2":700,"y2":688,"stroke":93,"strokeWidth":690,"style":691,"markerEnd":692},"434.4","472.4",[273,702],{"x1":703,"y1":688,"x2":704,"y2":688,"stroke":93,"strokeWidth":690,"style":691,"markerEnd":692},"583.2","621.2",[162,706,708],{"id":707},"verification","Verification",[262,710,714],{"className":711,"code":712,"language":713,"meta":267,"style":267},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","curl -s -H 'Accept-Encoding: br' -D - -o \u002Ftmp\u002Fa.br https:\u002F\u002Fexample.com\u002Fassets\u002Fapp.4f2a.js | grep -iE 'content-(encoding|type|length)'\nls -l dist\u002Fassets\u002Fapp.4f2a.js.br\n# trade-off: the transferred Content-Length should equal the .br file size; a\n# mismatch means something recompressed or decompressed it along the way.\n","bash",[33,715,716,757,768,773],{"__ignoreMap":267},[271,717,718,721,724,727,730,733,736,739,742,745,748,751,754],{"class":273,"line":274},[271,719,720],{"class":311},"curl",[271,722,723],{"class":390}," -s",[271,725,726],{"class":390}," -H",[271,728,729],{"class":295}," 'Accept-Encoding: br'",[271,731,732],{"class":390}," -D",[271,734,735],{"class":295}," -",[271,737,738],{"class":390}," -o",[271,740,741],{"class":295}," \u002Ftmp\u002Fa.br",[271,743,744],{"class":295}," https:\u002F\u002Fexample.com\u002Fassets\u002Fapp.4f2a.js",[271,746,747],{"class":284}," |",[271,749,750],{"class":311}," grep",[271,752,753],{"class":390}," -iE",[271,755,756],{"class":295}," 'content-(encoding|type|length)'\n",[271,758,759,762,765],{"class":273,"line":281},[271,760,761],{"class":311},"ls",[271,763,764],{"class":390}," -l",[271,766,767],{"class":295}," dist\u002Fassets\u002Fapp.4f2a.js.br\n",[271,769,770],{"class":273,"line":302},[271,771,772],{"class":277},"# trade-off: the transferred Content-Length should equal the .br file size; a\n",[271,774,775],{"class":273,"line":315},[271,776,777],{"class":277},"# mismatch means something recompressed or decompressed it along the way.\n",[162,779,781],{"id":780},"worked-example-a-static-documentation-site","Worked Example: A Static Documentation Site",[15,783,784,785,180,787,789],{},"A documentation site deployed 1,800 pages of static HTML plus assets to a self-managed nginx behind a CDN. nginx compressed with gzip level 6 per request; the CDN cached gzip variants. Adding Brotli 11 and Zopfli precompression to the build (adding 50 seconds to a 4-minute build) and enabling ",[33,786,229],{},[33,788,232],{}," reduced HTML transfer by 19% and JS by 17%. Origin CPU during cache refills dropped by 60%. LCP p75 improved by 90ms on mobile, mostly from smaller HTML.",[15,791,792],{},[64,793,71,796,71,799,71,802,71,804,71,807,71,809,71,814,71,821,71,825,71,829,71,832,71,836,71,840,71,844,71,846,71,849,71,851,71,854,71,857,71,861,71,863,71,865,71,867,71,869,71,872,71,876,71,878,71,880,71,882,71,885,71,888,71,892,71,894,71,897,71,899,71,902,71,905,71,909,71,911,71,913,71,915,71],{"viewBox":794,"width":67,"role":68,"ariaLabel":795,"style":70},"0 0 760 258","File types and whether they should be precompressed at build time.",[73,797],{"className":798,"x":77,"y":77,"width":67,"height":67,"fill":78},[76],[80,800,801],{},"Which files to precompress",[84,803,795],{},[73,805],{"x":88,"y":88,"width":89,"height":806,"rx":91,"fill":92,"stroke":93,"style":94},"256",[96,808,801],{"x":98,"y":99,"fill":93,"style":100},[73,810],{"x":98,"y":111,"width":811,"height":812,"rx":77,"fill":93,"stroke":93,"style":813},"181.7","30.0","fill-opacity:0.06;stroke-opacity:0.4",[96,815,820],{"x":816,"y":817,"fill":93,"style":818,"textAnchor":819},"38.0","75.5","font-size:12.5px;font-weight:700","start","File type",[73,822],{"x":823,"y":111,"width":824,"height":812,"rx":77,"fill":93,"stroke":93,"style":813},"209.7","261.1",[96,826,828],{"x":827,"y":817,"fill":93,"style":818,"textAnchor":634},"340.3","Precompress?",[73,830],{"x":831,"y":111,"width":824,"height":812,"rx":77,"fill":93,"stroke":93,"style":813},"470.9",[96,833,835],{"x":834,"y":817,"fill":93,"style":818,"textAnchor":634},"601.4","Why",[73,837],{"x":98,"y":838,"width":811,"height":812,"rx":77,"fill":92,"stroke":93,"style":839},"86.0","stroke-opacity:0.35",[96,841,843],{"x":816,"y":842,"fill":93,"style":818,"textAnchor":819},"105.5","JS, CSS, SVG",[73,845],{"x":823,"y":838,"width":824,"height":812,"rx":77,"fill":115,"stroke":115,"style":628},[96,847,848],{"x":827,"y":842,"fill":93,"style":639,"textAnchor":634},"yes",[73,850],{"x":831,"y":838,"width":824,"height":812,"rx":77,"fill":92,"stroke":93,"style":839},[96,852,853],{"x":834,"y":842,"fill":93,"style":639,"textAnchor":634},"text compresses 70-85%",[73,855],{"x":98,"y":856,"width":811,"height":812,"rx":77,"fill":92,"stroke":93,"style":839},"116.0",[96,858,860],{"x":816,"y":859,"fill":93,"style":818,"textAnchor":819},"135.5","Static HTML, JSON, XML",[73,862],{"x":823,"y":856,"width":824,"height":812,"rx":77,"fill":115,"stroke":115,"style":628},[96,864,848],{"x":827,"y":859,"fill":93,"style":639,"textAnchor":634},[73,866],{"x":831,"y":856,"width":824,"height":812,"rx":77,"fill":92,"stroke":93,"style":839},[96,868,96],{"x":834,"y":859,"fill":93,"style":639,"textAnchor":634},[73,870],{"x":98,"y":871,"width":811,"height":812,"rx":77,"fill":92,"stroke":93,"style":839},"146.0",[96,873,875],{"x":816,"y":874,"fill":93,"style":818,"textAnchor":819},"165.5","WASM",[73,877],{"x":823,"y":871,"width":824,"height":812,"rx":77,"fill":115,"stroke":115,"style":628},[96,879,848],{"x":827,"y":874,"fill":93,"style":639,"textAnchor":634},[73,881],{"x":831,"y":871,"width":824,"height":812,"rx":77,"fill":92,"stroke":93,"style":839},[96,883,884],{"x":834,"y":874,"fill":93,"style":639,"textAnchor":634},"compresses moderately",[73,886],{"x":98,"y":887,"width":811,"height":812,"rx":77,"fill":92,"stroke":93,"style":839},"176.0",[96,889,891],{"x":816,"y":890,"fill":93,"style":818,"textAnchor":819},"195.5","WOFF2, AVIF, WebP, JPEG",[73,893],{"x":823,"y":887,"width":824,"height":812,"rx":77,"fill":154,"stroke":155,"style":644},[96,895,896],{"x":827,"y":890,"fill":93,"style":639,"textAnchor":634},"no",[73,898],{"x":831,"y":887,"width":824,"height":812,"rx":77,"fill":92,"stroke":93,"style":839},[96,900,901],{"x":834,"y":890,"fill":93,"style":639,"textAnchor":634},"already compressed",[73,903],{"x":98,"y":904,"width":811,"height":812,"rx":77,"fill":92,"stroke":93,"style":839},"206.0",[96,906,908],{"x":816,"y":907,"fill":93,"style":818,"textAnchor":819},"225.5","Files under 1KB",[73,910],{"x":823,"y":904,"width":824,"height":812,"rx":77,"fill":154,"stroke":155,"style":644},[96,912,896],{"x":827,"y":907,"fill":93,"style":639,"textAnchor":634},[73,914],{"x":831,"y":904,"width":824,"height":812,"rx":77,"fill":92,"stroke":93,"style":839},[96,916,917],{"x":834,"y":907,"fill":93,"style":639,"textAnchor":634},"overhead exceeds savings",[162,919,921],{"id":920},"precompression-on-static-hosts-and-cdns","Precompression on Static Hosts and CDNs",[15,923,924,925,927],{},"Platforms differ. Some static hosts compress at the edge automatically with Brotli at good levels; precompressing yourself may not change much. Others serve files as uploaded, so precompression is required for Brotli. Some CDNs ignore origin ",[33,926,51],{}," and recompress. Test what users actually receive: compare the transferred size of a known asset against your locally computed Brotli 11 size. If they match, precompression is effective; if the transferred size is larger, something in the chain recompresses at a lower level.",[162,929,931],{"id":930},"making-precompression-part-of-ci","Making Precompression Part of CI",[15,933,934,935,40,937,939,940,943,944,947,948,950],{},"Treat precompressed outputs as part of the build contract. A CI step can verify that every text asset above the size threshold has ",[33,936,179],{},[33,938,183],{}," siblings, that each compressed file decompresses back to the original bytes, and that compressed sizes stay within budget. After deployment, a smoke test requests a few assets with ",[33,941,942],{},"Accept-Encoding: br"," and compares ",[33,945,946],{},"Content-Length"," with the size of the matching ",[33,949,179],{}," file in the build output. This catches the most common failure — compressed files present but not served — on the first deploy rather than months later.",[162,952,954],{"id":953},"common-mistakes","Common Mistakes",[167,956,957,963,969,978],{},[170,958,959,962],{},[173,960,961],{},"Precompressing but not serving the files."," Server configuration missing.",[170,964,965,968],{},[173,966,967],{},"Wrong Content-Type on compressed files."," Scripts and styles fail to load.",[170,970,971,974,975,977],{},[173,972,973],{},"Stale compressed files."," Old ",[33,976,179],{}," files served after source changes when names are not hashed.",[170,979,980,983],{},[173,981,982],{},"Compressing tiny files."," Overhead with no benefit; use a threshold.",[162,985,987],{"id":986},"edge-cases","Edge Cases",[15,989,990,993,994,997,998,1001],{},[173,991,992],{},"Unhashed files."," For files like ",[33,995,996],{},"index.html"," or ",[33,999,1000],{},"robots.txt",", regenerate compressed variants on every build and avoid long cache lifetimes.",[15,1003,1004,1007],{},[173,1005,1006],{},"Source maps."," Compress them too if served publicly; they are large text files.",[15,1009,1010,1013],{},[173,1011,1012],{},"WebAssembly."," WASM compresses moderately with Brotli; include it.",[15,1015,1016,1019],{},[173,1017,1018],{},"Service worker precaching."," Service workers store decompressed responses; precompression helps the network transfer, not cache storage.",[162,1021,1023],{"id":1022},"faq","FAQ",[1025,1026,1029,1033],"details",{"className":1027},[1028],"faq-item",[1030,1031,1032],"summary",{},"Why precompress instead of letting the server compress?",[15,1034,1035],{},"Precompression allows maximum compression levels at no runtime cost and removes CPU work from every request.",[1025,1037,1039,1042],{"className":1038},[1028],[1030,1040,1041],{},"What is Zopfli?",[15,1043,1044],{},"A gzip-compatible compressor that produces smaller gzip files than standard gzip at the cost of much slower compression — suitable for build-time use.",[1025,1046,1048,1051],{"className":1047},[1028],[1030,1049,1050],{},"Do I need both .br and .gz files?",[15,1052,1053],{},"Yes, for broad compatibility: Brotli for modern browsers, gzip for clients without Brotli support.",[1025,1055,1057,1060],{"className":1056},[1028],[1030,1058,1059],{},"Does precompression work with CDNs?",[15,1061,1062],{},"It depends on the CDN. Some pass through origin-compressed responses; others recompress. Verify transferred sizes.",[1025,1064,1066,1069],{"className":1065},[1028],[1030,1067,1068],{},"How much build time does Brotli 11 add?",[15,1070,1071],{},"Seconds for large bundles, less for small files. Most builds add well under a minute, and caching avoids recompressing unchanged hashed files.",[1025,1073,1075,1078],{"className":1074},[1028],[1030,1076,1077],{},"Should I precompress HTML?",[15,1079,1080],{},"Static HTML from site generators, yes. Server-rendered HTML is generated per request and must be compressed on the fly.",[1025,1082,1084,1087],{"className":1083},[1028],[1030,1085,1086],{},"Can I precompress with zstd as well?",[15,1088,1089,1090,1093],{},"Yes, if your server can serve ",[33,1091,1092],{},".zst"," files to clients that accept zstd. For static assets, Brotli 11 usually produces the smallest files.",[1025,1095,1097,1100],{"className":1096},[1028],[1030,1098,1099],{},"Does precompression change cache behaviour?",[15,1101,1102,1103,1106],{},"No. Responses are cached per encoding just as with on-the-fly compression; set ",[33,1104,1105],{},"Vary: Accept-Encoding"," and long-lived immutable caching on hashed files.",[1025,1108,1110,1113],{"className":1109},[1028],[1030,1111,1112],{},"What about images embedded as base64 in CSS or JS?",[15,1114,1115],{},"Base64 data compresses poorly and inflates text files by about a third. Move large embedded images to separate files so they can be cached and served in efficient formats.",[1025,1117,1119,1122],{"className":1118},[1028],[1030,1120,1121],{},"Is precompression useful for small sites?",[15,1123,1124],{},"Yes, if the host serves the files you upload. It is a one-time build change that makes every text download smaller and removes compression work from the server, regardless of traffic volume.",[1025,1126,1128,1131],{"className":1127},[1028],[1030,1129,1130],{},"How do I precompress in a webpack build?",[15,1132,1133],{},"Add compression-webpack-plugin twice, once with the Brotli algorithm and once with gzip, filtered to text file extensions and a minimum size. The plugin writes compressed files next to the originals for the server to pick up.",[162,1135,1137],{"id":1136},"related","Related",[167,1139,1140,1147,1154],{},[170,1141,1142,1146],{},[19,1143,1145],{"href":1144},"\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fenabling-brotli-on-nginx-and-cdns\u002F","Enabling Brotli on nginx and CDNs"," — serving configuration.",[170,1148,1149,1153],{},[19,1150,1152],{"href":1151},"\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fcompression-dictionary-transport\u002F","Compression dictionary transport"," — beyond single-file compression.",[170,1155,1156,1160],{},[19,1157,1159],{"href":1158},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced caching strategies & CDN architecture"," — hashed filenames and immutable caching that make precompression safe.",[1162,1163,1165],"script",{"type":1164},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Precompress Static Assets at Build Time\",\n  \"description\": \"How to precompress static text assets at maximum compression during the build, serve the precompressed files correctly, and verify the result.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Add a precompression step\",\n      \"text\": \"Use a bundler plugin (for example vite-plugin-compression, compression-webpack-plugin) or a small script after the build.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Serve precompressed files\",\n      \"text\": \"For Node.js, express-static-gzip or similar middleware serves precompressed variants.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Upload to object storage correctly\",\n      \"text\": \"When serving from S3-like storage behind a CDN, upload the compressed body under the original name with Content-Encoding: br, or use the CDN's encoding-aware variants.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Exclude already-compressed files\",\n      \"text\": \"Skip images, fonts (WOFF2), video, audio and archives.\"\n    }\n  ]\n}\n",[1162,1167,1168],{"type":1164},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Precompress Static Assets at Build Time\",\n  \"description\": \"How to precompress static text assets at maximum compression during the build, serve the precompressed files correctly, and verify the result.\",\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\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fprecompressing-static-assets-at-build-time\u002F\"\n  }\n}\n",[1162,1170,1171],{"type":1164},"\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\": \"Network & Server Response Optimization\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fnetwork-protocol-optimization\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Compression: Brotli & Zstandard\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Precompressing Static Assets at Build Time\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fprecompressing-static-assets-at-build-time\u002F\"\n    }\n  ]\n}\n",[1173,1174,1175],"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 .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sn2CQ, html code.shiki .sn2CQ{--shiki-default:#024C1A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold;--shiki-light:#024C1A;--shiki-light-font-weight:bold}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}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);}",{"title":267,"searchDepth":281,"depth":281,"links":1177},[1178,1179,1180,1186,1187,1188,1189,1190,1191,1192,1193],{"id":164,"depth":281,"text":165},{"id":213,"depth":281,"text":214},{"id":251,"depth":281,"text":252,"children":1181},[1182,1183,1184,1185],{"id":256,"depth":302,"text":257},{"id":482,"depth":302,"text":483},{"id":564,"depth":302,"text":565},{"id":582,"depth":302,"text":583},{"id":707,"depth":281,"text":708},{"id":780,"depth":281,"text":781},{"id":920,"depth":281,"text":921},{"id":930,"depth":281,"text":931},{"id":953,"depth":281,"text":954},{"id":986,"depth":281,"text":987},{"id":1022,"depth":281,"text":1023},{"id":1136,"depth":281,"text":1137},"How to precompress static text assets at maximum compression during the build, serve the precompressed files correctly, and verify the result.","md",{"slug":1197,"type":1198,"breadcrumb":1199,"datePublished":1206,"dateModified":1206},"precompressing-static-assets-at-build-time","article",[1200,1202,1203,1204],{"name":1201,"url":180},"Home",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1205},"\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fprecompressing-static-assets-at-build-time\u002F","2026-10-06",true,"\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fprecompressing-static-assets-at-build-time",{"title":1210,"description":1211},"Precompressing Static Assets with Brotli at Build Time","Generate .br and .gz files for JS, CSS, SVG and HTML at maximum levels during the build, serve them with brotli_static, and stop compressing per request.","network-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fprecompressing-static-assets-at-build-time\u002Findex","W1kt_XlhY5Sxv3zjTr9itW9dJS7D3Ld8PO5pLV0biiQ",[1215,1218],{"title":1145,"path":1216,"stem":1217,"children":-1},"\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fenabling-brotli-on-nginx-and-cdns","network-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fenabling-brotli-on-nginx-and-cdns\u002Findex",{"title":1219,"path":1220,"stem":1221,"children":-1},"Zstandard Content-Encoding for the Web","\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fzstandard-content-encoding-for-the-web","network-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fzstandard-content-encoding-for-the-web\u002Findex",1791308073262]