kit
kit copied to clipboard
Use import maps for better caching
Describe the problem
Suppose you have three pages, /one, /two and /three, that each depend on Widget.svelte. Because that component is used by multiple pages, Vite will create a chunk for it with a hashed name: chunks/Widget-abc123.js.
Since routes are also code-split, the pages will have their own chunks — pages/one-def234.js, pages/two-ghi345.js and pages/three-jkl456.js. Each will have an import declaration like this:
import Widget from '../chunks/Widget-abc123.js';
The hashing allows us to treat these assets as immutable, which means that repeat visitors won't have to redownload those chunks as long as they don't change.
But.
One day we change Widget.svelte without changing /one, /two or /three. Because the hashes are based on content, chunks/Widget-abc123.js is now chunks/Widget-mno567.js. That's fine — we want users to redownload that chunk — but it means that the import declaration now looks like this...
import Widget from '../chunks/Widget-mno567.js';
...which means that the page chunk hashes must also change, even though they're otherwise identical. Suddenly, the user must redownload four chunks (more, in fact, since the main entry point containing the route manifest is also tainted) even though only one module changed.
Obviously this problem isn't unique to SvelteKit, but it's a problem we're well-placed to solve.
Describe the proposed solution
Import maps solve this problem. By mapping stable identifiers to the hashed assets...
<script type="importmap">
{
"imports": {
"chunks/Widget": "/_app/chunks/Widget-abc123.js",
"pages/one": "/_app/pages/one-def234.js",
...
}
}
</script>
...we can import chunks like so:
import Widget from 'chunks/Widget';
Now, changes to Widget.svelte won't taint its consumers — all we need to do is update the import map.
Wrinkles:
- Import maps aren't yet supported in all browsers. Happily, there's a production-ready solution: https://github.com/guybedford/es-module-shims
- Generating import declarations with stable identifiers while still generating hashed assets might be tricky — I'm not sure how to do that with Vite. I'm confident we could figure it out though
- The import map could conceivably grow quite large, perhaps even to the point where the trade-off isn't worth it
Alternatives considered
No response
Importance
nice to have
Additional Information
No response
Maybe related: https://github.com/vitejs/vite/issues/2483
Proof of concept for Vite+Rollup: https://github.com/vitejs/vite/issues/6773#issuecomment-1308048405 There's also SystemJS approach in https://github.com/rollup/rollup/issues/3407
With the release of Safari 16.4, import maps are now supported across all major browsers.
https://caniuse.com/import-maps
Safari 16.4 isn't widely adopted yet though.
I actually managed to scaffold a prototype of this. I haven't had too much time to play around with it, but the repo can be found here:
https://github.com/konnorRogers/asset-mapper
Theres an example SvelteKit app in there that I used to generate an example manifest that looks like this:
JSON output
// .svelte-kit/output/client/asset-mapper-manifest.json
{
"_app/immutable/assets/svelte-logo.svg": "_app/immutable/assets/svelte-logo-87df40b8fbb4afb4.svg",
"_app/immutable/assets/github.svg": "_app/immutable/assets/github-1ea8d62ee9f90811.svg",
"_app/immutable/assets/fira-mono-greek-ext-400-normal.woff2": "_app/immutable/assets/fira-mono-greek-ext-400-normal-9e2fe623052dabee.woff2",
"_app/immutable/assets/fira-mono-greek-400-normal.woff2": "_app/immutable/assets/fira-mono-greek-400-normal-a8be01cef8d13a05.woff2",
"_app/immutable/assets/svelte-welcome.webp": "_app/immutable/assets/svelte-welcome-c18bcf5a7a25fc1d.webp",
"_app/immutable/assets/fira-mono-cyrillic-ext-400-normal.woff2": "_app/immutable/assets/fira-mono-cyrillic-ext-400-normal-3df7909e625102bb.woff2",
"_app/immutable/assets/fira-mono-cyrillic-400-normal.woff2": "_app/immutable/assets/fira-mono-cyrillic-400-normal-c7d433fd8d89f891.woff2",
"_app/immutable/assets/fira-mono-latin-ext-400-normal.woff2": "_app/immutable/assets/fira-mono-latin-ext-400-normal-6bfabd307779c11b.woff2",
"_app/immutable/entry/app.js": "_app/immutable/entry/app-f338ff0a3089d9ab.js",
"_app/immutable/assets/fira-mono-all-400-normal.woff": "_app/immutable/assets/fira-mono-all-400-normal-1e3b098cc2d20d35.woff",
"_app/immutable/assets/svelte-welcome.png": "_app/immutable/assets/svelte-welcome-6c300099eb59ca6c.png",
"_app/immutable/entry/start.js": "_app/immutable/entry/start-d0598b62ed40ea9b.js",
"_app/immutable/entry/_layout.svelte.js": "_app/immutable/entry/_layout.svelte-de54b5df8c99a43e.js",
"_app/immutable/entry/_page.svelte.js": "_app/immutable/entry/_page.svelte-cae5b5b27bafd7da.js",
"_app/immutable/entry/error.svelte.js": "_app/immutable/entry/error.svelte-e622c1032969775e.js",
"_app/immutable/entry/about-page.svelte.js": "_app/immutable/entry/about-page.svelte-d99e7b253ffd1e16.js",
"_app/immutable/assets/fira-mono-latin-400-normal.woff2": "_app/immutable/assets/fira-mono-latin-400-normal-e43b3538e39a85a0.woff2",
"_app/immutable/entry/sverdle-how-to-play-page.svelte.js": "_app/immutable/entry/sverdle-how-to-play-page.svelte-a5d1d2c678b115ed.js",
"_app/immutable/entry/about-page.js.js": "_app/immutable/entry/about-page.js-bf64cf6e88336878.js",
"_app/immutable/entry/sverdle-how-to-play-page.js.js": "_app/immutable/entry/sverdle-how-to-play-page.js-bf4e30b459881312.js",
"_app/immutable/entry/sverdle-page.svelte.js": "_app/immutable/entry/sverdle-page.svelte-63f64dcadd55dac5.js",
"_app/immutable/entry/_page.js.js": "_app/immutable/entry/_page.js-9f067d6c4e964d6d.js",
"_app/immutable/chunks/index.js": "_app/immutable/chunks/index-90a53736ddfa4113.js",
"_app/immutable/chunks/singletons.js": "_app/immutable/chunks/singletons-b51f803857cdde4d.js",
"_app/immutable/chunks/1.js": "_app/immutable/chunks/1-35de6d94f03ca829.js",
"_app/immutable/chunks/parse.js": "_app/immutable/chunks/parse-300c9616221d5453.js",
"_app/immutable/chunks/3.js": "_app/immutable/chunks/3-52195e7b83748fe8.js",
"_app/immutable/chunks/4.js": "_app/immutable/chunks/4-cf47d3ec2700026c.js",
"_app/immutable/chunks/2.js": "_app/immutable/chunks/2-a00365ffa7eb34d9.js",
"_app/immutable/chunks/0.js": "_app/immutable/chunks/0-01514bbbcf4ecad9.js",
"_app/immutable/chunks/stores.js": "_app/immutable/chunks/stores-45f9f67b65489a5d.js",
"_app/immutable/chunks/5.js": "_app/immutable/chunks/5-ede663e6a4df8158.js",
"_app/immutable/chunks/_page2.js": "_app/immutable/chunks/_page2-100d4dff2b757243.js",
"_app/immutable/chunks/index2.js": "_app/immutable/chunks/index2-3506a163e244535d.js",
"_app/immutable/chunks/_page.js": "_app/immutable/chunks/_page-1c6540c96d702a52.js",
"_app/immutable/chunks/environment.js": "_app/immutable/chunks/environment-6b4c8de700c75d5f.js",
"_app/immutable/chunks/_page3.js": "_app/immutable/chunks/_page3-100d4dff2b757243.js",
"_app/immutable/assets/_page.css": "_app/immutable/assets/_page-89a9e780fc0f784e.css",
"_app/immutable/assets/_page2.css": "_app/immutable/assets/_page2-265a38f05eb328b2.css",
"_app/immutable/assets/_layout.css": "_app/immutable/assets/_layout-746118a189509aa7.css",
"_app/immutable/assets/_page3.css": "_app/immutable/assets/_page3-9d50104935bef3f8.css",
"_app/version.json": "_app/version-fa2e8ab57482a8c7.json"
}
So while it doesn't directly write the importmaps, a simple server side helper could write the importmap. The benefit of course being the manifest can be used for other things if needed by having a logical reference to a hashed path. This is roughly how Rails asset pipeline works for anyone familiar.
<%= asset_path("app.js") %>
# => app-[hash].js
The obvious caveats are is I really don't know how well collided filenames get handled.