Repository navigation
Minify whitespace in JSON <script> bodies (application/json, ld+json, speculationrules, importmap) #291
Description
What happens
A <script> whose type is JSON passes through byte for byte, indentation and all. minify-html-common/src/spec/script.rs carries JAVASCRIPT_MIME_TYPES and nothing beside it, so application/json, application/ld+json, speculationrules and importmap are raw text to the minifier. Measured on 0.18.1 with minify_js and minify_css both on:
<script type="speculationrules">
{
"prerender": [
{ "where": { "href_matches": "/garage/*" }, "eagerness": "moderate" }
]
}
</script>comes out with every newline, every indent and the double space intact.
Who this helps, measured both ways
I measured a whitespace strip (delete whitespace outside strings, touch nothing else) on two populations, and the split is sharp enough to be worth stating up front:
| blocks | raw | after strip | |
|---|---|---|---|
| hand-authored documents (aadhar.sh source, 41 documents) | 78 | 269,737 B | 231,487 B, -14.2% |
| bbc.com | 2 | 154,232 B | 0.00% |
| stripe.com | 2 | 226,790 B | 0.00% |
| theverge.com | 3 | 319,953 B | 0.00% |
| vercel.com | 7 | 2,629 B | 0.00% |
So this does nothing for a site whose JSON comes out of a serializer, which is most large sites, and saves about 930 B of source per document for someone who writes JSON-LD or speculation rules readably in a template. That is the same argument that already justifies minifying an inline <style>: the author writes it legibly and the minifier ships it small.
Why this belongs in minify-html rather than in a JSON tool
Two reasons, and the second one is measurable on a live page today.
Only the HTML tokenizer can find it. The JSON is a substring delimited by raw-text rules, where </script >, </script/> and </script\nfoo> end the element and </scriptfoo> does not. Anything that wants to minify it has to tokenize HTML first.
The output constraint is HTML's, not JSON's. "</script>" is a legal JSON string and an illegal thing to emit between script tags. A correct JSON minifier is free to produce it, so piping an extracted body through one is not safe in general. That is not hypothetical: theverge.com ships an application/json block containing an Instagram embed whose markup is escaped as </script>, and a JSON.parse + JSON.stringify round trip turns it into a live </script>:
source: ...embed.js\"></script>\n</div...
reserialized: ...embed.js\"></script>\n</div></figure>\n","placement...
Implementation note: strip, do not reserialize
Reserializing is the tempting implementation and it is the wrong one, for three reasons that are all visible in the numbers above.
It is where that </script> comes from. It also renumbers: any literal a double cannot hold changes value, so 9007199254740993 ships as 9007199254740992, which matters for ids and amounts. And the bytes it appears to save are exactly the escapes that keep JSON safe inside HTML. Reserializing saves 440 B on bbc.com, 1,770 B on stripe.com and 5,435 B on theverge.com, and on inspection that is almost entirely < and > being unescaped.
A whitespace strip has none of those properties: it only deletes bytes, so it can neither create a closing sequence nor change a value. It needs no dependency either, unlike the oxc and lightningcss passes already in the crate.
Precedent
The crate already describes itself as an "Extremely fast and smart HTML + JS + CSS minifier" and vendors oxc_minifier and lightningcss to make good on it. JSON is the one content type inside <script> that is declined, and I could not find a stated reason or a prior issue for it.
Happy to send a PR (a JSON_MIME_TYPES set beside the JavaScript one, plus the strip and tests) if the feature is wanted.
Activity