kit icon indicating copy to clipboard operation
kit copied to clipboard

Use import maps for better caching

Open Rich-Harris opened this issue 3 years ago • 2 comments

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

Rich-Harris avatar Apr 01 '22 16:04 Rich-Harris

Maybe related: https://github.com/vitejs/vite/issues/2483

bluwy avatar Apr 02 '22 08:04 bluwy

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

jacekkarczmarczyk avatar Nov 09 '22 11:11 jacekkarczmarczyk

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.

JReinhold avatar Mar 30 '23 06:03 JReinhold

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.

KonnorRogers avatar Apr 01 '23 18:04 KonnorRogers