mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-23 15:53:46 +00:00
1 commit
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1bf4be70e3
|
Studio: budget the JavaScript that runs before the first screen (#8964)
* Studio: budget the JavaScript that runs before the first screen * Report the eager chunk count without budgeting it * Close the ways this gate could pass without measuring anything Three of them were reachable. Run through a symlinked checkout, the main-module guard compared process.argv[1] (the path as typed) against import.meta.url (the real path), they disagreed, and the script exited 0 having done nothing. That is anything under /tmp on macOS. Both sides now go through realpath. With build.modulePreload: false Vite emits the entry script and no preload links. The empty-set check did not fire, and a real build of that shape measured 424 KB of a 5,207 KB startup path and reported 4.8 MB to spare. The guard now needs two chunks from Vite, and it counts scripts and links together so the layout Vite uses when the entry module is nothing but imports (one script per chunk, no links) still passes. A chunk named in index.html but missing from the build threw out of readFileSync, exiting 1 with a stack, indistinguishable from a budget failure. It now reports the file and exits 2. Also: a parser-blocking classic script is charged to startup. index.html loads public/theme-boot.js that way, before the module graph, and it was outside the budget it belongs in. defer/async and cross-origin scripts are not counted. Matching is now case-insensitive and treats rel as a token list, so a partial match cannot quietly shrink the measured set. Total on this build goes from 5,207.2 KB raw / 1,496.2 KB gzip over 44 chunks to 5,208.3 KB / 1,496.8 KB over 45, still inside budget. * Judge a script tag on whether the browser runs it type="application/javascript" and the other JavaScript MIME types are classic scripts too. Matching only text/javascript would have let one sit outside the budget, which is the same silent under-measurement the rest of this is about. importmap and application/json still do not count: they are not code that runs. * Charge deferred scripts, and size non-asset files as they are served A deferred classic script runs after parsing but before DOMContentLoaded, in document order with the module entry, which is itself deferred. It is on exactly the timeline this budgets, so excluding it left a way to move startup JavaScript out of the budget without moving it off the startup path. Only async is excluded now, and async is the attribute tested because it wins when a tag carries both. The transfer column was gzip for everything, but the backend gzips the /assets mount only; anything else goes out through a plain FileResponse. theme-boot.js is the one such file today, and charging it gzip understated what actually crosses the wire. Non-asset files are now charged their raw size, and the column is called transfer rather than gzip, which is what it has always been measuring. 5,208.3 KB raw / 1,497.3 KB transfer over 45 chunks, still inside budget. * Anchor attribute matching on whitespace, not a word boundary A hyphen is a word boundary, so the \btype pattern matched inside data-type and read the decoy in preference to the real attribute. The same held for data-src, data-href and data-rel. Reproduced: a tag with data-type alongside type="module" dropped the entry entirely while its preload links kept the shape guard satisfied, which is a silent under-measurement of exactly the kind the rest of this exists to prevent. Every attribute in a tag is preceded by whitespace, the tag name included, so whitespace is the boundary HTML actually gives us. * Read index.html with a tag scanner instead of a regex Searching the whole tag text for an attribute name found it in other attributes' values, and treating the first > as the end of the tag ended it inside a quoted value. <script data-mode="load async later" src="/theme-boot.js"> <script data-note="a > b" type="module" src="/assets/entry.js"> The first has no async attribute and is parser-blocking, so it belongs in the budget; the second's entry is /assets/entry.js. The old code dropped both, and in each case the rest of the build still satisfied the shape guard, so the gate reported a comfortable pass over a startup path it had not measured. Attributes are now parsed off each start tag: > ends a tag only outside a quoted value, and a name is only read where a name can begin. Comments and inline script bodies are skipped, which a stateful scanner has to do to stay in sync, and which also stops a commented-out script being charged. * Count async scripts that are asked to block rendering The exclusion of async assumed it has no ordering relationship to the first screen. blocking="render" creates exactly that relationship, and it is the documented way to keep a boot script off the parser without letting the unthemed page paint, which is what theme-boot.js is for. Per the spec an element is potentially render-blocking if its blocking tokens set contains render, OR if it is implicitly potentially render-blocking; the async carve-out lives only in the implicit half, so the explicit attribute applies to an async script too. Measured rather than assumed. Holding /slow.js for two seconds moved first contentful paint from 28 ms to 2,020 ms in Chromium 151, which also reports the request renderBlockingStatus as blocking, and from 11 ms to 2,009 ms in WebKit 26.5. Firefox has not shipped it and treats the script as plain async. Left uncounted, such a script delays the first screen by its whole fetch and evaluation while the entry and preloads keep the shape guard satisfied. * Recognise every JavaScript MIME essence a browser still runs The set held the four spellings anyone writes today, but the rule it cites is the spec essence list, which has sixteen. The other twelve are not dead letters: measured in Chromium 151, application/x-javascript, text/jscript, text/javascript1.5, text/livescript, application/x-ecmascript and text/x-javascript all execute. So a startup script tagged with one of those was fetched and run by the browser and left out of the budget, while the entry and preloads kept the shape guard satisfied. The same probe confirms the two exclusions already relied on here: text/javascript; charset=utf-8 and application/json do not execute, because the attribute is matched against the whole essence string and a parameter makes it match nothing. The list is frozen upstream, so it does not grow. * Require a module entry before trusting the preload links The shape guard counted entry scripts and preload links together, so 48 links carried it on their own and a build whose entry was misread still measured and passed. The entry chunk is the largest single thing on the startup path, so that is the worst place for the total to paper over a gap. Preloads without an entry is not a shape Vite emits. A modulepreload link exists to announce the entry a static import closure hangs off, so links surviving while the entry does not means the entry was read wrong. The total still decides whether this is a code-split build, which keeps the inlined-entry layout passing: several module scripts and no links at all is a complete measurement. This is the residue of two mis-parses fixed earlier in this branch. Both did their damage the same way, by dropping the entry while the links kept the guard satisfied, so the invariant is worth stating outright rather than relying on the parser never being wrong again. |