JavaScript filters
Last updated Edit this page View as Markdown
Minify, combine, inline, and defer JavaScript in mod_pagespeed 2.1. Directives, defaults, and the trade-offs for each JS filter on Apache, nginx, and IIS.
On this page
Overview
mod_pagespeed 2.1 includes filters for JavaScript minification, combining, inlining, and deferring execution. Three JS filters are CoreFilters: rewrite_javascript, combine_javascript, and inline_javascript.
Quick reference
| Filter | Core | OFB | Description | Safe |
|---|---|---|---|---|
rewrite_javascript | Yes | Yes | Minifies JS | Yes |
rewrite_javascript_external | Yes | Yes | Implied by rewrite_javascript, external files only | Yes |
rewrite_javascript_inline | Yes | Yes | Implied by rewrite_javascript, inline scripts only | Yes |
combine_javascript | Yes | No | Combines multiple scripts | Yes |
inline_javascript | Yes | No | Inlines small scripts | Yes |
defer_javascript | No | No | Defers execution | Test first |
outline_javascript | No | No | Externalizes large inline scripts | Experimental |
include_js_source_maps | No | No | Preserves source maps | Yes |
extend_cache_scripts | Yes | No | Content-hashed script URLs with a one-year cache | Yes |
canonicalize_javascript_libraries | No | No | Swaps recognized libraries for a canonical URL | Dangerous set |
deterministic_js | No | No | Deterministic Date and Math.random, for measuring | Dangerous set |
disable_javascript | No | No | Wraps scripts in noscript, for measuring | Dangerous set |
strip_scripts | No | No | Removes all scripts, for measuring | Dangerous set |
make_show_ads_async | No | No | Converts showads.js to async adsbygoogle.js | Deprecated |
make_google_analytics_async | No | No | No-op: targeted the retired ga.js | Deprecated |
IIS syntax
On IIS, use the same filter names with the pagespeed prefix in pagespeed.config (no semicolons):
pagespeed EnableFilters rewrite_javascript,combine_javascript
pagespeed JsInlineMaxBytes 2048
See IIS configuration for the full file format reference.
rewrite_javascript
Full guide → · Also: rewrite_javascript_external, rewrite_javascript_inline
What it does
The three filters on this section share one minifier. It removes comments and collapses whitespace; identifiers, string literals, regular expressions and property names are emitted unchanged. It keeps a line break wherever automatic semicolon insertion could otherwise merge two statements into one. How safe JavaScript minification handles automatic semicolon insertion has the details.
/* before: sum a shopping cart */
function cartTotal(cart) {
var total = 0; // running sum
for (var i = 0; i < cart.items.length; i++) {
total = total + cart.items[i].price;
}
return total;
}
/* after */
function cartTotal(cart){var total=0;for(var i=0;i<cart.items.length;i++){total=total+cart.items[i].price;}return total;}
rewrite_javascript is the compound CoreFilter for JavaScript minification: enabling it switches on the external and the inline sub-filter together, so every script on the page is minified wherever it lives. It is also part of OptimizeForBandwidth, the level that minifies resources in place without changing their URLs. To minify only one scope, enable rewrite_javascript and disable the other sub-filter by name. Under CoreFilters the compound is on already. A script delivered with an integrity attribute is left untouched by the whole family, because subresource integrity pins the file’s bytes. Since v1.15.0+r21 the tokenizer-based minifier is the only one behind rewrite_javascript, and files that use template literals (backtick strings) minify normally; IE conditional-compilation comments (/*@ ... @*/) and a leading #! line are the only comments it keeps. Live demo: rewrite_javascript.
rewrite_javascript_external handles the external half. Each <script src> file on a domain the module is authorized to fetch is minified and served from a rewritten .pagespeed.jm. URL with a long cache lifetime, so repeat visitors download the minified file once and keep it; the original file on disk is never modified. In OptimizeForBandwidth mode the minified bytes replace the original response in place and the URL stays as authored. Minification saves the most on hand-formatted source; a file a bundler already minified gains nothing and only costs rewrite time. rewrite_javascript_external runs as part of the compound and can also be enabled on its own.
Enabled on its own, rewrite_javascript_external leaves inline <script> blocks exactly as authored; minifying those is the inline sub-filter’s job. Files on domains the configuration does not authorize keep their original URLs as well, so a page that mixes own and third-party scripts sees only the own files rewritten.
rewrite_javascript_inline minifies the contents of <script> blocks in the HTML itself, in place. No URL changes and nothing new is cached; the smaller script simply rides along inside every page view. Blocks whose type does not denote executable JavaScript, such as JSON-LD data islands and HTML templates, are left as they are. Inline scripts tend to be short, so the saving per block is small; the filter’s job inside the compound is to leave no script unminified. Like the external half, rewrite_javascript_inline is in CoreFilters and in OptimizeForBandwidth, and it can be enabled by itself.
rewrite_javascript_inline never moves a block or changes when it runs; it only shrinks the text between <script> and </script>. On pages whose HTML is served many times between deploys that small saving repeats on every serve, which is where the filter earns its place.
Risks
- The minifier never renames identifiers; the historical failure mode is a script that inspects its own source text, for example through
Function.prototype.toString(), and reacts to the changed formatting. - Verify with the
X-Mod-Pagespeedresponse header and a?PageSpeedFilters=-rewrite_javascriptcomparison; Is it working? has the steps. rewrite_javascriptignores the deprecatedUseExperimentalJsMinifierdirective, which is accepted for compatibility and logs a warning at configuration load (ModPagespeedUseExperimentalJsMinifieron Apache,pagespeed UseExperimentalJsMinifieron nginx). Remove it from your configuration.
Configuration
Apache:
ModPagespeedEnableFilters rewrite_javascript
Nginx:
pagespeed EnableFilters rewrite_javascript;
combine_javascript
What it does
combine_javascript concatenates consecutive external scripts into one file, loads the combined file from a .pagespeed.jc. URL, and replaces each original tag with a small inline script, eval(…), that runs that member in order. A page that loads five scripts back to back makes one request instead of five. The scripts run in their original order, so dependencies between them keep working. Live demo: combine_javascript.
<!-- before -->
<script src="/js/jquery.js"></script>
<script src="/js/carousel.js"></script>
<script src="/js/forms.js"></script>
<!-- after -->
<script src="/js/jquery.js+carousel.js+forms.js.pagespeed.jc.HASH.js"></script><script>eval(mod_pagespeed_HASH1);</script>
<script>eval(mod_pagespeed_HASH2);</script>
<script>eval(mod_pagespeed_HASH3);</script>
When it helps and when it does not
Combining was designed for HTTP/1.1, where a browser opens only a few connections per host and extra requests queue. Over HTTP/2 and HTTP/3 requests multiplex over one connection, so the saving shrinks to per-request overhead. The costs cut the other way too: the combined file is refetched in full when any member changes, and the browser cannot run the first script until the whole combined file has arrived. On a legacy page loading a dozen small files, combining still pays. On a page with two or three scripts over HTTP/2, measure with the filter on and off before keeping it.
How it decides
Only consecutive, synchronously executing external scripts join a group. An inline <script>, a script with async or defer, a type="module" script, a script of an unknown type, and a script carrying integrity= each end the current group, as does other markup between two script tags. Modules are excluded because the combination evaluates member scripts inside one shared file, which cannot represent a module’s isolated scope and deferred execution. A group stops growing when the combined uncompressed contents would pass MaxCombinedJsBytes (default 92160), and every member must come from a domain the module is authorized to fetch. Pages whose Content-Security-Policy forbids eval or inline scripts are not combined, because the browser would block the inline eval tags.
Risks
- If a page misbehaves after combining, load it with
?PageSpeedFilters=-combine_javascriptand compare behavior and the console; theX-Mod-Pagespeedresponse header confirms whether the filter ran. Is it working? covers the routine.
Configuration
Apache:
ModPagespeedEnableFilters combine_javascript
ModPagespeedMaxCombinedJsBytes 92160
Nginx:
pagespeed EnableFilters combine_javascript;
pagespeed MaxCombinedJsBytes 92160;
inline_javascript
What it does
inline_javascript replaces a small external script with an inline <script> block that holds the file’s contents, so the browser skips a request. The element keeps its place in the page, so execution order does not change. Live demo: inline_javascript.
<!-- before: one extra request for a 1.4 KB file -->
<script src="/js/newsletter-popup.js"></script>
<!-- after: the file's contents sit inside the page -->
<script>document.addEventListener("DOMContentLoaded",function(){var d=document.getElementById("newsletter");d&&setTimeout(function(){d.hidden=!1},4e3)});</script>
When it helps and when it does not
Inlining pays when a script is tiny: for a few hundred bytes, the request with its headers and round trip costs more than the bytes it fetches. The trade runs the other way as files grow or get shared. An inlined script is not cached on its own, so it downloads again with every page view, and a file inlined into ten pages is transferred ten times. Scripts that several pages share, and anything well above the default threshold, are better served external with a long cache lifetime. inline_javascript is a CoreFilter, so this trade is already live on a default install; the threshold is the knob.
How it decides
Only external scripts whose contents are no larger than JsInlineMaxBytes (default 2048 bytes) qualify, and only files on domains the module is authorized to fetch. A script with async or defer, or with the IE-specific for and event attributes, is left external: those attributes change when the script runs, and an inline block cannot express that timing. Module scripts (type="module") are never inlined, since inlining would change how their relative imports resolve. Nothing is inlined when the page’s Content-Security-Policy forbids inline scripts. Everything else about the element, including its position in the document, stays as authored.
Risks
- The page grows by the script’s size on every view, so keep
JsInlineMaxBytessmall. Raising it to inline a large file usually costs more than the saved request returns. - Verify with the
X-Mod-Pagespeedresponse header and a?PageSpeedFilters=-inline_javascriptcomparison; Is it working? has the steps.
Configuration
Apache:
ModPagespeedEnableFilters inline_javascript
ModPagespeedJsInlineMaxBytes 2048
Nginx:
pagespeed EnableFilters inline_javascript;
pagespeed JsInlineMaxBytes 2048;
defer_javascript
Not a core filter. Test thoroughly before enabling. Defers execution of all JavaScript until after the page finishes loading. This can dramatically improve initial render time but will break scripts that rely on executing during page parse (e.g., document.write).
Apache:
ModPagespeedEnableFilters defer_javascript
Nginx:
pagespeed EnableFilters defer_javascript;
Risks
- Scripts using
document.writewill fail. - Scripts that expect to run before
DOMContentLoadedmay break. - Order-dependent scripts may execute in unexpected order.
- Inserts a
<noscript>redirect by default. Disable withSupportNoScriptEnabled false. - Since v1.15.0+r18, with
HonorCsp(default on) the filter stands down entirely on pages whoseContent-Security-Policyforbids inline scripts — deferral relies on injected inline JavaScript, which such a policy would block. - Since v1.15.0+r21,
defer_javascript— along withdisable_javascript,defer_iframe,fix_reflow, and the sharedsupport_noscriptfallback — switches off for clients identified as automated, including clients that send noUser-Agent; those clients receive the page’s normal authored script markup. Search-engine crawlers are included deliberately. An automated client presenting a browser’s exact user-agent string is indistinguishable from that browser and still receives the deferred form.
outline_javascript
What it does
outline_javascript is the inverse of inline_javascript: it moves a large inline <script> block out of the HTML into its own JavaScript file, served from a rewritten _.pagespeed.jo. URL with a long cache lifetime, and replaces the block with a <script src> pointing at it. The generated element is a copy of the original with the src added, so attributes such as id survive the move. Experimental and not a core filter; enable it by name. Live demo: outline_javascript.
<!-- before: 12 KB of script ride inside every page response -->
<script type="text/javascript" id="large">
window.app = { ... };
</script>
<!-- after: the script is fetched once and kept in the browser cache -->
<script type="text/javascript" id="large" src="/_.pagespeed.jo.HASH.js"></script>
When it helps and when it does not
A block of inline JavaScript is re-sent with every page view, and on pages whose HTML is generated per request those bytes cannot be cached at all. Outlining moves them into a file the browser fetches once and keeps, which is the same trade outline_css makes for stylesheets. It loses when the HTML itself is cached, when the block differs from page to page, or on pages visited once, because the first view pays an extra request it did not have before. Execution keeps its place in the document: an outlined script without defer or async still runs at the element’s position, once the file has arrived. But defer and async on an inline script are ignored by browsers and take effect once the script is external, so a script carrying either runs later once outlined than it did inline. Most sites are better served by keeping script in real files or bundling them there at build time.
How it decides
Only an inline script (one without src) classified as JavaScript is a candidate, and only when its text is at least JsOutlineMinBytes (default 3000 bytes). Two kinds are always left inline: a script carrying an integrity attribute, because the hash browsers ignore on an inline script would become enforced against the outlined bytes, and an inline module script, because outlining it would move import resolution from the document’s base URL to the generated file’s URL and change import.meta.url. A script the parser cannot hold as one unit, because a flush arrives mid-script or a stray tag sits inside it, is left alone rather than guessed at.
Risks
- The outlined script is an extra request on the first view; on single-view pages that costs more than the HTML bytes it saves.
- Markup that expects the
<script>element’s contents to sit in the page, for example a script that reads its own source text, sees asrcinstead. - Verify with the
X-Mod-Pagespeedresponse header and a?PageSpeedFilters=-outline_javascriptcomparison; Is it working? has the steps.
Configuration
Apache:
ModPagespeedEnableFilters outline_javascript
ModPagespeedJsOutlineMinBytes 3000
Nginx:
pagespeed EnableFilters outline_javascript;
pagespeed JsOutlineMinBytes 3000;
include_js_source_maps
Not a core filter. Preserves JavaScript source maps through minification by adding a //# sourceMappingURL= comment pointing to the original source map. Enable this if you need to debug minified JavaScript in production.
Apache:
ModPagespeedEnableFilters include_js_source_maps
Nginx:
pagespeed EnableFilters include_js_source_maps;
extend_cache_scripts
Core filter, one of the three members of extend_cache. Rewrites <script src> URLs to content-hashed .pagespeed.ce. URLs served with a one-year Cache-Control max-age: browsers keep scripts for a year, and a changed file gets a new URL, so there is nothing to purge. Use it when the origin cannot set long cache lifetimes itself; under CoreFilters it is already on through extend_cache. Disabling it leaves extend_cache_css and extend_cache_images on. Live demo: extend_cache.
Apache:
ModPagespeedEnableFilters extend_cache_scripts
Nginx:
pagespeed EnableFilters extend_cache_scripts;
Dangerous and deprecated filters
The module keeps these names so that an existing configuration still loads. The filters in the dangerous set are never switched on by RewriteLevel AllFilters and exist for measurement and testing, not for production traffic; the deprecated ones do nothing.
canonicalize_javascript_libraries
Replaces a <script src> that matches a known library (recognized by size and hash through the Library directive) with the library’s canonical URL on a shared CDN, so visitors reuse a copy already in their browser cache. In the dangerous set: the module ships no library table of its own any more, cross-site caches are partitioned in current browsers, and a canonical URL you do not control is a dependency you do not control. Use it only with your own Library entries and your own CDN. Live demo: canonicalize_javascript_libraries.
pagespeed Library 105527 ltVVzzYxo0 //cdn.example.com/js/prototype.1.6.1.0.js;
pagespeed EnableFilters canonicalize_javascript_libraries;
deterministic_js
Injects a script that makes Date and Math.random return deterministic values, so two loads of a page produce the same output and can be compared byte for byte. For measurement and regression testing only; it changes the behavior of every script on the page. In the dangerous set.
pagespeed EnableFilters deterministic_js;
disable_javascript
Wraps every <script> in <noscript> so no script on the page runs, to measure what the page looks like and costs without JavaScript. For measurement only. In the dangerous set.
pagespeed EnableFilters disable_javascript;
strip_scripts
Removes every <script> element from the page, the more drastic variant of disable_javascript for measuring the no-script baseline. For measurement only. In the dangerous set.
pagespeed EnableFilters strip_scripts;
make_show_ads_async
Rewrites synchronous showads.js ad snippets to the asynchronous adsbygoogle.js form so the ads stop blocking rendering. It targets a deprecated AdSense integration; convert the snippets in your templates instead. Live demo: make_show_ads_async.
pagespeed EnableFilters make_show_ads_async;
make_google_analytics_async
Deprecated and a no-op: it rewrote the retired ga.js snippet to its asynchronous form. The name is accepted so old configurations load; remove it.
Tuning parameters
| Parameter | Default | Description |
|---|---|---|
JsInlineMaxBytes | 2048 | Max JS file size (bytes) to inline |
JsOutlineMinBytes | 3000 | Min inline JS size (bytes) to externalize |
Apache:
ModPagespeedJsInlineMaxBytes 2048
ModPagespeedJsOutlineMinBytes 3000
Nginx:
pagespeed JsInlineMaxBytes 2048;
pagespeed JsOutlineMinBytes 3000;