Home
Software
TIL
Contact Me
vite /rollup manualChunks
In MS-DOS days programmers were limited to 640 kB of code. When their programs became bigger than that they had to implement tricky code to load / unload code chunks on demand.
In web apps we’re not limited by size but it’s still a good idea to strive for the smallest possible JavaScript bundle. We can do it by extracting rarely used pieces of functionality into separate JavaScript chunks and import them lazily. This makes the app faster to load, faster to parse and run.
For example, in Edna I use markdown-it and highlight.js library only in a certain scenario. By putting it in it’s separate chunk, I save almost 1 MB of uncompressed JavaScript in main bundle. Faster to download, faster to run.
../dist/assets/markdownit-hljs-DbctGXX9.js          1,087.33 kB │ gzip: 358.42 kB
To split in chunks you configure rollup in vite.config.js:

function manualChunks(id) {
	// partition files into chunks
}

export default defineConfig({
	build: {
		rollupOptions: {
			output: {
				manualChunks: manualChunks,
			}
		}
	}
}
manualChunks() functions takes a path of the file (don’t know why everyone calls it an id). If you return a string for a given path, you tell rollup to bundle that file in a given chunk. If you return nothing (i.e. undefined) rollup will decide on how to chunk automatically, most likely putting everything into a single chunk
It gets called for .css files, .js files and probably others.
Here’s my hard won wisdom:
Seems simple enough:
function manualChunks(id) {
  console.log(id);

  const chunksDef = [
    ["/@zip.js/zip.js/", "zipjs"],
    ["/prettier/", "prettier"],
    // markdown-it and highlight.js are used together in askai.svelte
    [
      "/markdown-it/",
      "/markdown-it-anchor/",
      "/highlight.js/",

	  "/entities/",
      "/linkify-it/",
      "/mdurl/",
      "/punycode.js/",
      "/uc.micro/",

      "markdownit-hljs",
    ],
  ];
  for (let def of chunksDef) {
    let n = def.length;
    for (let i = 0; i < n - 1; i++) {
      if (id.includes(def[i])) {
        return def[n - 1];
      }
    }
  }

  // bundle all other 3rd-party modules into a single vendor.js module
  if (id.includes("/node_modules/")) {
    return "vendor";
  }
  // when we return undefined, rollup will decide
}
This is real example from Edna. I’ve put zip.js, prettier and markdown-it + markdown-it-anchor + highlight.js into their own chunks, which I lazily import.
Things to note:
There were 2 remaining problems.
I also had this in my code:
import "highlight.js/styles/github.css";
manualChunks was called for "highlight.js/styles/github.css" to which I returned markdownit-hljs so it was put in its own chunk. Which was too much because I didn’t want to lazy import a small CSS file, so I told rollup to put all .css files in main CSS chunk:
function manualChunks(id) {
  // pack all .css files in the same chunk
  if (id.endsWith(".css")) {
    return;
  }

  // ... rest of the code
}
There was one more thing that was a big pain in the ass to debug.
To verify things are properly chunked I opened Dev Tools in Chrome and looked at network tab. To my surprise, markdownit-hljs was loaded immediately.
After lots of debugging and research: turns out that vite bundles some helper functions. Because I didn’t specify explicitly which chunk they should go into, rollup decided to put them in markdownit-hljs chunk. Because main chunk was using that function, it had to import it, defeating my cunning plan to load it lazily later.
The fix was to direct those known helper function into vendor bundle:
function manualChunks(id) {
	// ... other code
	// bundle all other 3rd-party modules into a single vendor.js module
	if (
		id.includes("/node_modules/") ||
		ROLLUP_COMMON_MODULES.some((commonModule) => id.includes(commonModule))
	) {
		return "vendor";
	}
	// ... rest of the code
}
You can read more at:
Things might still go wrong. I got one invalid build that created a chunk that wouldn’t parse in the browser.
For that reason I suggest to start simple: empty manualChunks() function that packs everything in one chunk. Then add desired chunks one by one and after each change.
And how to lazy import things?
let markdownIt = (await import("markdown-it")).default;
is equivalent to static import:
import markdownIt from "markdown-it";
Read more about lazy imports.
til webdev javascript programming
Jul 19 2025

Home
Software
TIL
Contact Me