Skip to content

Minify JS removes significant whitespace inside ES6 template literals, breaking Lit/lit-html components #8834

Description

@Cairon

Description

The JS minifier treats the contents of ES6 template literals as code and strips "insignificant" whitespace. In template literals used as HTML templates — the standard pattern for Lit/lit-html, uhtml, HTM, etc. — that whitespace is significant.

On an already-minified 271 KB vendor bundle, the pass removes 24 spaces, all of them inside template literals, for a total gain of 103 bytes (0.04%). Five of those removals produce invalid HTML and silently break the components.

Two failure modes:

A. Structural — the space between a tag name and the next attribute is removed:

- <rndzz-mentions .designConfiguration="${x}">
+ <rndzz-mentions.designConfiguration="${x}">

The parser swallows the whole run as a tag name. lit-html's template scanner then loses its position and stops emitting $lit$ binding markers for the rest of that template literal, so every following property binding lands in the DOM as a dead literal attribute:

<rndzz-box-offer-list
  .rendering="lit$065088049$"
  .designconfiguration="lit$065088049$"
  .runtimeconfig="lit$065088049$"
  .tabidx="lit$065088049$">
</rndzz-box-offer-list>

The component receives undefined for every input and renders empty. No exception, no console error, all network requests succeed — the widget just goes blank.

B. Rendered text — spaces in HTML text content are removed, gluing words and inline elements together:

- …)} </span>${fe(…        - …)} <span class="…
+ …)}</span>${fe(…         + …)}<span class="…

6 of the 8 removals I aligned individually are of this kind, so this is likely already corrupting other sites unnoticed.

Steps to reproduce

Both files are public and same-origin, so the diff is directly checkable:

URL (on https://leveltech.fr) Bytes
Source /wp-content/cache/56979e14-38e5-4b72-99e7-cbd9c8d3d9d5/3d47e038480c291e1803b6f822d67adf44b4f876.js 270,986
WP Rocket output /wp-content/cache/min/1/wp-content/cache/56979e14-…/3d47e038….js 270,883
$ grep -o '<rndzz-[a-z-]*\.' source.js   | wc -l   # 0
$ grep -o '<rndzz-[a-z-]*\.' minified.js | wc -l   # 5
$ grep -o '<rndzz-[a-z-]* \.' source.js  | wc -l   # 30
$ grep -o '<rndzz-[a-z-]* \.' minified.js| wc -l   # 25

Live page: https://leveltech.fr/test-samsung-galaxy-buds4-2026/ — the offer list inside <affilizz-rendering-component> renders empty. Loading with ?nowprocket=1 is a quick confirmation.

Minimal repro candidate (shape of the two confirmed cases at source offsets 165444 and 172827), single line:

const f=(e)=>y`<rndzz-mentions .designConfiguration="${e.cfg}"></rndzz-mentions>`;

Expected

Bytes between the opening and closing backtick of a template literal are string data and must not be altered, except inside ${…} substitutions where normal code rules apply.

Actual

24 spaces removed from inside template literals; 5 of them produce invalid HTML and silently break rendering.

Environment

  • WP Rocket: (version TBD)
  • Setting: File Optimization → Minify JavaScript files (output under /wp-content/cache/min/1/)
  • Affected library: Lit 3 (lit-html 3.3.2, reactive-element 2.1.2, lit-element 4.2.2)
  • The bundle is served locally by a "self-host third-party JS" plugin (/* Loaded from local cache - … */ header), then minified by WP Rocket.

Ruled out during diagnosis: duplicate Lit copies (single instance of each package), Trusted Types, patched DOM APIs, ShadyDOM, load-order races (a widget injected after full page load reproduces identically), and data/config (a different publication ID that works on another site also fails here).

Proof that the dead bindings are the sole cause — assigning the four properties by hand renders the component correctly and immediately:

const box = document.querySelector('affilizz-rendering-component')
  .shadowRoot.querySelector('rndzz-rendering')
  .shadowRoot.querySelector('rndzz-box');
const bol = box.shadowRoot.querySelector('rndzz-box-offer-list');
Object.assign(bol, {
  rendering: box.rendering,
  designConfiguration: box.designConfiguration,
  runtimeConfig: box.runtimeConfig,
  tabIdx: 0,
});
// → 5 offers render

Suggested fix

  1. Treat template-literal content as opaque in the tokenizer; recurse only into ${…}, with a depth counter for nested templates.
  2. If the tokenizer is regex-based: " and ' inside template-literal content must not open a string context.
  3. Optionally skip files that are already minified (single line, high average line length) — negligible gain, exactly this risk.

Related

#5897 — honouring data-noptimize / data-cfasync="false" would let vendors opt their bundles out without every site owner configuring an exclusion by hand. Useful mitigation, not a substitute for the tokenizer fix.

Current workaround: add the bundle path to Excluded JavaScript Files.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    module: minify JSpriority: lowIssues that can waitseverity: minorDefect that does not affect functionalitytype: bugIndicates an unexpected problem or unintended behavior

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions