{"id":14722396,"url":"https://github.com/wille/vite-preload","last_synced_at":"2025-04-10T20:32:31.339Z","repository":{"id":253329203,"uuid":"842884612","full_name":"wille/vite-preload","owner":"wille","description":"Speed up your Vite application by preloading server rendered lazy modules and stylesheets as early as possible","archived":false,"fork":false,"pushed_at":"2025-04-10T18:44:54.000Z","size":393,"stargazers_count":28,"open_issues_count":1,"forks_count":4,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-10T19:58:34.199Z","etag":null,"topics":["css","esmodules","react","ssr","vite","vite-plugin"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wille.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-15T10:02:53.000Z","updated_at":"2025-04-10T18:44:58.000Z","dependencies_parsed_at":"2024-08-26T17:05:05.321Z","dependency_job_id":"6d7db2f0-73bb-4aec-b0a7-fd0de8c820e4","html_url":"https://github.com/wille/vite-preload","commit_stats":null,"previous_names":["wille/vite-preload"],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wille%2Fvite-preload","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wille%2Fvite-preload/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wille%2Fvite-preload/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wille%2Fvite-preload/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wille","download_url":"https://codeload.github.com/wille/vite-preload/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248289780,"owners_count":21078920,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["css","esmodules","react","ssr","vite","vite-plugin"],"created_at":"2024-09-14T05:02:06.425Z","updated_at":"2025-04-10T20:32:31.329Z","avatar_url":"https://github.com/wille.png","language":"TypeScript","funding_links":[],"categories":["Plugins"],"sub_categories":["Framework-agnostic Plugins"],"readme":"[![NPM package](https://img.shields.io/npm/v/vite-preload.svg?style=flat-square)](https://www.npmjs.com/package/vite-preload)\n\n# vite-preload\n\nThis plugin will significantly speed up your server rendered Vite application by preloading dynamically imported React components and their stylesheets as early as possible. It will also ensure that the stylesheet of the lazy component is included in the initial HTML to avoid a Flash Of Unstyled Content (FOUC).\n\nRead more about [Nested modules and loading performance](https://web.dev/articles/script-evaluation-and-long-tasks#trade-offs_and_considerations)\n\nThis plugin is different to [vite-plugin-preload](https://www.npmjs.com/package/vite-plugin-preload) because it evaluates used modules at render time rather than including every single module in the HTML at build time.  \n\nIncludes functionality similar to [loadable-components](https://loadable-components.com/) where you can create `\u003clink\u003e` tags and `Link` headers for the rendered chunks.\n\n#### See [`./playground`](./playground/) for a basic setup with preloading\n\n## Explainer\n\nAny lazy imported React component and its CSS will only be loaded once the parent module has been loaded and executed. This will lead to a Flash Of Unstyled Content (FOUC) and a delay in loading the required chunks for the React client.\n\nThis plugin will collect which modules are rendered on the server and help you inject `\u003clink\u003e` tags in the HTML `\u003chead\u003e` and `Link` preload headers for which you can use with  [103 Early Hints](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103) which you can use to make the browser work before React even started rendering on the server.\n\n## Without preloading\n\nYou can see the async chunks being loaded after the JS started executing (indicated by the red line)\n![Before](./doc/before.png)\n\n## With preloading\n\nYou can see that the async chunks are loaded directly and instantly available once the JS starts executing\n![After](./doc/after.png)\n\n## Installation\n\n```\n$ npm install vite-preload\n```\n\n### `vite.config.ts`\n\nSetup the Vite plugin to inject a helper function in your dynamically imported React components that will make lazy components detected on SSR.\n\n\n\n```ts\nimport preloadPlugin from 'vite-preload/plugin';\nexport default defineConfig({\n    plugins: [\n        preloadPlugin(),\n        react(),\n        // ...\n    ],\n});\n```\n\n\n\u003e [!IMPORTANT]\n\u003e Preloading does not show up in development mode. In development mode, Vite will inject CSS using inline style tags on demand, which will always come with some Flash Of Unstyled Content (FOUC). [Read more](https://github.com/wille/vite-preload/pull/1)\n\n\u003e [!NOTE]\n\u003e If some of your modules does not get preloaded, make sure `build.rollupOptions.output.experimentalMinChunkSize` is not set. Rollup might merge chunks so they are not mapped in the manifest and can't be found when calculating what chunks to preload.\n\n\n---\n\n### Setup on the server in your render handler\n\n```tsx\nimport { ChunkCollectorContext, createChunkCollector, preloadAll } from 'vite-preload';\n\nasync function handler(req, res) {\n    // Preload all async chunks on the server otherwise the first render will trigger the suspense fallback because the lazy import has not been resolved\n    await preloadAll();\n\n    const collector = createChunkCollector({\n        manifest: './dist/client/.vite/manifest.json',\n        entry: 'index.html',\n    })\n\n    const template = process.env.NODE_ENV === 'production' ? await fs.readFile('./dist/client/index.html', 'utf8') : undefined;\n    const [head, tail] = template.split('\u003c!-- app-root --\u003e')\n\n    // Write a HTTP 103 Early Hint way before React even started rendering with the entry chunks which we know we will be needed by the client.\n    // Chrome will only pick it up when running with HTTP/2 so try Firefox if you want to test it.\n    res.writeEarlyHints({\n        link: collector.getLinkHeaders()\n    })\n\n    const { pipe } = renderToPipeableNodeStream(\n        \u003cChunkCollectorContext collector={collector}\u003e\n            \u003cApp /\u003e\n        \u003c/ChunkCollectorContext\u003e,\n        {\n            // onShellReady also works but it will miss chunks that React does not consider a part of the shell, like server components or lazy components on the server that has not resolved. React will always suspend and show the fallback ui for lazy components on the first server render unless you preload all lazy components using something like react-lazy-with-preload first.\n            onAllReady() {\n                const linkTags = collector.getTags();\n                const linkHeaders = collector.getLinkHeaders();\n\n                // The link header below now contains \n                res.writeHead(200, {\n                    'content-type': 'text/html',\n                    'link': linkHeaders\n                })\n\n                // Inject \u003clink rel=modulepreload\u003e and \u003clink rel=stylesheet\u003e in the head. Without this the CSS for any lazy component would be loaded after the app has and cause a Flash of Unstyled Content (FOUC).\n                res.write(head.replace('\u003c/head\u003e', `${linkTags}\\n\u003c/head\u003e`););\n\n                const transformStream = new Transform({\n                    transform(chunk, encoding, callback) {\n                        res.write(chunk, encoding);\n                        callback();\n                    },\n                });\n\n                pipe(transformStream)\n\n                transformStream.on('finish', () =\u003e {\n                    res.end(tail);\n                });\n            }\n        }\n    );\n}\n```\n\n## Options\n\n`createChunkCollector(options)`\n- `manifest`: string/object - path to the vite manifest or the manifest object (defaults to `./dist/client/.vite/manifest.json`)\n- `entry`: string - entry name, defaults to `index.html`\n- `preloadFonts`: true/false - Include fonts, true by default\n- `preloadAssets`: true/false - Include assets like images and fonts\n\n`ChunkCollector`\n- `getTags()`: Returns a string with `\u003clink\u003e` tags to be included in the HTML head\n- `getLinkHeaders()`: Returns a list with `Link` header values\n\n## Migrating from `loadable-components`\n\nReplace all \n\n```tsx\nimport loadable from '@loadable/component'\nloadable(() =\u003e import('./module'))\n```\nwith\n```tsx\nimport { lazy } from 'vite-preload'\nlazy(() =\u003e import('./module'))\n```\nand evaluate if it performs well enough for your use case.\n\nLook into the examples below for other ways to optimize your app.\n\n## Usage with `React.lazy`\n\nReact.lazy works with Server Rendering using the React Streaming APIs like [renderToPipeableStream](https://react.dev/reference/react-dom/server/renderToPipeableStream)\n\nvite-preload exports a `React.lazy` wrapper that supports preloading using `Component.preload()` and `preloadAll()`\n\n```tsx\nimport { Suspense } from 'react';\nimport { lazy, preloadAll } from 'vite-preload';\n\nconst Card = lazy(() =\u003e import('./Card'));\n\nfunction App() {\n    return (\n        \u003cdiv\u003e\n            \u003cSuspense fallback={\u003cp\u003eSuspending...\u003c/p\u003e}\u003e\n                \u003cCard /\u003e\n            \u003c/Suspense\u003e\n        \u003c/div\u003e\n    )\n}\n```\n\n### Server \n\n\u003e [!NOTE]\n\u003e React.lazy has some undeterministic behaviour in server rendering.\n\u003e\n\u003e - The first render on the server will always trigger the suspense fallback. Use `await preloadAll()` on the server to preload all async components before rendering the app.\n\u003e - Larger components in large projects that takes time to load will trigger the suspense fallback on the client side, even if the component is server rendered. This might be avoided by preloading all async routes before hydration with `await preloadAll()` or skipping the top level Suspense boundary.\n\n\n## Usage with `react-router`\n\nReact Router v6 supports lazy routes using the `lazy` prop on the `Route` component.\n\nWhen navigating on the client side to a lazy route, the document will not repaint until the lazy route has been loaded, avoiding a flash of white like when using loadable-components with react-router. This might have a negative impact on your [INP](https://web.dev/articles/inp) metric, so you might want to use deterministic loading of lazy routes like preloading the next step in the user flow or preloading them all in the background after the primary modules has been loaded.\n\n\u003e [!NOTE]\n\u003e When hydrating a lazy route, the server rendered HTML will be thrown away, cause a hydration mismatch error, then load and render again.\n\u003e To prevent this so you will need preload all the lazy routes rendered by the server like in the example below.\n\u003e See https://reactrouter.com/en/main/guides/ssr#lazy-routes\n\n```tsx\nimport { Route } from 'react-router'\nimport { hydrateRoot } from 'react-dom/server'\n\nfunction lazyRoute(dynamicImportFn: () =\u003e Promise\u003cany\u003e) {\n  return async () =\u003e {\n    const { default: Component } = await dynamicImportFn()\n    return { Component }\n  }\n}\n\nconst routes = (\n    \u003cRoute lazy={lazyRoute(() =\u003e import('./Card'))} /\u003e\n)\n\nfunction loadLazyRoutes() {\n    const matches = matchRoutes(routes, window.location);\n\n    if (!matches) {\n        return\n    }\n\n    const promises = matches.map(match =\u003e {\n        if (!m.route.lazy) {\n            return\n        }\n        const routeModule = await m.route.lazy!()\n\n        m.route.Component = routeModule.Component\n        delete m.route.lazy\n        Object.assign(m.route, {\n            ...routeModule,\n            lazy: undefined,\n        })\n    });\n\n    await Promise.all(promises)\n}\n\nasync function main() {\n    await loadLazyRoutes()\n\n    ReactDOM.hydrateRoot(\n        \u003cRouterProvider router={router} /\u003e,\n        document.getElementById('root')\n    )\n}\n```\n\n# How it works\n\n## Plugin\nThe plugin will inject a hook in every dynamically imported React component. This is because we need to map the module id to the corresponding client chunks from the generated Vite [manifest.json](https://vitejs.dev/guide/backend-integration).\n\n\nBelow is what the plugin would inject in a module imported  using `React.lazy(() =\u003e import('./lazy-component'))`:\n```ts\n// src/lazy-component.tsx\nimport { __collectModule } from 'vite-preload/__internal';\nimport styles from './lazy-component.module.css';\nexport default function Lazy() {\n    __collectModule('src/lazy-component.ts'); // Injected by the plugin\n    return \u003cdiv className={styles.div}\u003eHi\u003c/div\u003e\n}\n```\n\nThe manifest entry for this chunk would look similar to\n\n```json\n\"src/lazy-component.tsx\": {\n    \"file\": \"assets/lazy-component-CbXeDPe5.js\",\n    \"name\": \"lazy-component\",\n    \"src\": \"src/lazy-component.tsx\",\n    \"isDynamicEntry\": true,\n    \"imports\": [\n        \"_vendor-1b2b3c4d.js\",\n    ],\n    \"css\": [\n        \"assets/lazy-component-DHcjhLCQ.css\"\n    ]\n}\n```\n\n\n## Server\nThe React server uses the context provider to catch these hook calls and map them to the corresponding client chunks extracted from the manifest\n\n```tsx\nimport { ChunkCollectorContext, preloadAll } from 'vite-preload';\n\nconst collector = createChunkCollector({\n    manifest: './dist/client/.vite/manifest.json',\n    entry: 'index.html',\n});\n\n// Preload all async components before rendering the app to avoid the first render to trigger the suspense fallback\nawait preloadAll();\n\nrenderToPipeableNodeStream(\n    \u003cChunkCollectorContext collector={collector}\u003e\n        \u003cApp /\u003e\n    \u003c/ChunkCollectorContext\u003e,\n    {\n        onAllReady() {\n            collector.getChunks()\n            // app.94184122.js\n            // app.94184122.css\n            // lazy-module.94184122.js\n            // lazy-module.94184122.css\n        }\n    }\n)\n```\n\n\n## Example HTTP response\n\n1. The 103 Early Hint will preload the entry chunks because they are known to be needed by the client. It's sent before React starts doing anything.\n2. The 200 OK response headers also contains the chunks of the lazy JS and CSS that were rendered.\n3. The head of the document uses the stylesheets, executes the primary module, and leaves the async modules as preloads for the browser to instantly use when a dynamic import is used.\n\n```html\nHTTP/2 103\nlink: \u003c/assets/index-CG7aErjv.js\u003e; rel=modulepreload; crossorigin\nlink: \u003c/assets/index-Be6T33si.css\u003e; rel=preload; as=style; crossorigin\n\nHTTP/2 200\ncontent-type: text/html; charset=utf-8\nlink: \u003c/assets/index-CG7aErjv.js\u003e; rel=preload; as=module; crossorigin\nlink: \u003c/assets/index-Be6T33si.css\u003e; rel=preload; as=style; crossorigin\nlink: \u003c/assets/Card.tsx\u003e; rel=modulepreload; crossorigin\nlink: \u003c/assets/Card.css\u003e; rel=preload; as=style; crossorigin\n\n\u003chtml\u003e\n    \u003chead\u003e\n        ...\n        \u003cscript type=\"module\" crossorigin src=\"/assets/index-CG7aErjv.js\"\u003e\u003c/script\u003e\n        \u003clink rel=modulepreload href=\"/assets/Card.tsx\" crossorigin\u003e\n        \u003clink rel=\"stylesheet\" crossorigin href=\"/assets/index-Be6T33si.css\"\u003e\n        \u003clink rel=stylesheet href=\"/assets/Card.css\" crossorigin\u003e\n        ...\n    \u003c/head\u003e\n    \u003cbody\u003e\n        ...\n    \u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Read more in the Vite documentation\n\n- [Backend Integration](https://vitejs.dev/guide/backend-integration.html)\n- [Server Side Rendering](https://vitejs.dev/guide/ssr.html)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwille%2Fvite-preload","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwille%2Fvite-preload","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwille%2Fvite-preload/lists"}