Skip to content

Commit 8d39420

Browse files
dominikgbenmccannbluwy
authored
feat: enable prebundling by default (#494)
* feat: enable prebundleSvelteLibraries by default * docs: add information about switching from deep imports to package imports for prebundled * docs: update prebundling faq * docs: update changeset to remove outdated advice * docs: Apply suggestions from code review Co-authored-by: Ben McCann <[email protected]> * docs: improve recommendations * fix: make sure prebundleSvelteLibraries is not used for build or ssr * docs: Apply suggestions from code review Co-authored-by: Ben McCann <[email protected]> * docs: Apply suggestions from code review Co-authored-by: Bjorn Lu <[email protected]> * docs: update follow-up wording after previous changes * fix: only enable prebundleSvelteLibraries for dev, log warnings if it is enabled during build or ssr * docs: update default value in docs * fix: remove warning for ssr.optimizeDeps, we don't add the svelte esbuild plugin for that so it does whatever, but won't break because of prebundleSvelteLibraries * Apply suggestions from code review Co-authored-by: Bjorn Lu <[email protected]> Co-authored-by: Ben McCann <[email protected]> Co-authored-by: Bjorn Lu <[email protected]>
1 parent 079a7b8 commit 8d39420

File tree

8 files changed

+198
-47
lines changed

8 files changed

+198
-47
lines changed

.changeset/calm-rules-push.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': minor
3+
---
4+
5+
enable `prebundleSvelteLibraries` during dev by default to improve page loading for the dev server.
6+
7+
see the [FAQ](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#what-is-going-on-with-vite-and-pre-bundling-dependencies) for more information about `prebundleSvelteLibraries` and how to tune it.

docs/config.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,13 @@ A [picomatch pattern](https://github.com/micromatch/picomatch), or array of patt
204204
### prebundleSvelteLibraries
205205

206206
- **Type:** `boolean`
207-
- **Default:** `false`
207+
- **Default:** `true` for dev, `false` for build
208+
209+
Enable [Vite's dependency prebundling](https://vitejs.dev/guide/dep-pre-bundling.html) for Svelte libraries.
210+
211+
This option improves page loading for the dev server in most applications when using Svelte component libraries.
208212

209-
Force Vite to pre-bundle Svelte libraries. Setting this `true` should improve initial page load performance, especially when using large Svelte libraries. See the [FAQ](./faq.md#what-is-going-on-with-vite-and-pre-bundling-dependencies) for details of the pre-bundling implementation.
213+
See the [FAQ](./faq.md#what-is-going-on-with-vite-and-pre-bundling-dependencies) for details and how to fine-tune it for huge libraries.
210214

211215
## Experimental options
212216

docs/faq.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,81 @@ For reference, check out [windicss](https://github.com/windicss/vite-plugin-wind
9999

100100
### What is going on with Vite and `Pre-bundling dependencies:`?
101101

102-
Pre-bundling dependencies is an [optimization in Vite](https://vitejs.dev/guide/dep-pre-bundling.html). It is required for CJS dependencies, as Vite's development server only works with ES modules on the client side. Importantly for Svelte libraries and ESM modules, prebundling combines component libraries into a single file to speed up the initial page load. Try setting the [`prebundleSvelteLibraries`](./config.md#prebundleSvelteLibraries) option to `true` to speed things up. This will likely be enabled by default in future version of the plugin.
102+
Prebundling dependencies is an [optimization in Vite](https://vitejs.dev/guide/dep-pre-bundling.html).
103+
104+
> We only use prebundling during **development**, the following does not apply to or describe the built application
105+
106+
It is required for CJS dependencies, as Vite's development server only works with ES modules on the client side.
107+
Importantly for Svelte libraries and ES modules, it also reduces the number of http requests when you load a page from the dev server and caches files so subsequent starts are even faster.
108+
109+
The way prebundling Svelte libraries affects your dev-server load times depends on the import style you use, index or deep:
110+
111+
#### Index imports
112+
113+
Offers better DX but can cause noticable delays on your machine, especially for libraries with many files.
114+
115+
```diff
116+
import { SomeComponent } from 'some-library'
117+
+ only one request per library
118+
+ intellisense for the whole library after first import
119+
- compiles the whole library even if you only use a few components
120+
- slower build and dev-server ssr
121+
```
122+
123+
#### Deep imports
124+
125+
Offers snappier dev and faster builds for libraries with many files at the expense of some DX
126+
127+
```diff
128+
import SomeComponent from 'some-library/src/SomeComponent.svelte'
129+
+ compiles only the components you import
130+
+ faster build and dev-server ssr
131+
- one request per import can slow down initial load if you use a lot of components
132+
- intellisense only for imported components
133+
```
134+
135+
#### Rewriting imports with plugins or preprocessors
136+
137+
**Do not use it in combination with prebundling!**
138+
139+
Prebundling works by reading your `.svelte` files from disk and scanning them for imports. It cannot detect
140+
added/changed/removed imports and these then cause extra requests, delays and render the prebundled files from the initial scan moot.
141+
If you prefer to use these tools, please exclude the libraries you use them with from prebundling.
142+
143+
#### Excluding libraries from prebundling
144+
145+
If you want to disable prebundling for a single library, use `optimizeDeps.exclude`
146+
147+
```js
148+
// vite.config.js
149+
export default defineConfig({
150+
optimizeDeps: {
151+
exclude: ['some-library'] // do not pre-bundle some-library
152+
}
153+
});
154+
```
155+
156+
Or disable it for all Svelte libraries
157+
158+
```js
159+
// svelte.config.js
160+
export default {
161+
vitePlugin: {
162+
prebundleSvelteLibraries: false
163+
}
164+
};
165+
```
166+
167+
#### Recommendations
168+
169+
There is no golden rule, but you can follow these recommendations:
170+
171+
1. **Never** combine plugins or preprocessors that rewrite imports with prebundling
172+
2. Start with index imports and if your dev-server or build process feels slow, check compile stats to see if switching to deep imports can improve the experience.
173+
3. Do not mix deep and index imports for the same library, use one style consistently.
174+
4. Use different import styles for different libraries where it helps. E.g. deep imports for the few icons of that one huge icon library, but index import for the component library that is heavily used.
175+
176+
#### I get a warning `Incompatible options: prebundleSvelteLibraries ...`
177+
178+
This warning only occurs if you use non-default settings in your vite config that can cause problems in combination with prebundleSvelteLibraries.
179+
You should not use prebundleSvelteLibraries during build or for ssr, disable one of the incompatible options to make that warning (and subsequent errors) go away.
Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
1-
import { getText, isBuild, readVitePrebundleMetadata } from '~utils';
1+
import { getText } from '~utils';
22

33
test('should render component imported via svelte field in package.json', async () => {
44
expect(await getText('#test-id')).toBe('svelte field works');
55
});
6-
7-
if (!isBuild) {
8-
test('should optimize nested cjs deps of excluded svelte deps', () => {
9-
const metadataFile = readVitePrebundleMetadata();
10-
const metadata = JSON.parse(metadataFile);
11-
const optimizedPaths = Object.keys(metadata.optimized);
12-
expect(optimizedPaths).not.toContain('e2e-test-dep-svelte-nested');
13-
expect(optimizedPaths).not.toContain('e2e-test-dep-svelte-simple');
14-
expect(optimizedPaths).toContain(
15-
'e2e-test-dep-svelte-nested > e2e-test-dep-svelte-simple > e2e-test-dep-cjs-only'
16-
);
17-
});
18-
}

packages/e2e-tests/package-json-svelte-field/vite.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { defineConfig } from 'vite';
44
export default defineConfig(({ command, mode }) => {
55
return {
66
plugins: [svelte()],
7+
optimizeDeps: {
8+
exclude: ['e2e-test-dep-scss-only']
9+
},
710
build: {
811
// make build faster by skipping transforms and minification
912
target: 'esnext',
Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,28 @@
1-
import { browserLogs, getText, isBuild, readVitePrebundleMetadata } from '~utils';
1+
import {
2+
browserLogs,
3+
editFile,
4+
getText,
5+
isBuild,
6+
readVitePrebundleMetadata,
7+
waitForServerRestartAndReloadPage
8+
} from '~utils';
29

3-
test('should not have failed requests', async () => {
10+
async function expectPageToWork() {
411
browserLogs.forEach((msg) => {
512
expect(msg).not.toMatch('404');
613
});
7-
});
8-
9-
test('should render Hybrid import', async () => {
1014
expect(await getText('#hybrid .label')).toBe('dependency-import');
11-
});
12-
13-
test('should render Simple import', async () => {
14-
expect(await getText('#simple .label')).toBe('dependency-import');
15-
});
16-
17-
test('should render Exports Simple import', async () => {
18-
expect(await getText('#exports-simple .label')).toBe('dependency-import');
19-
});
20-
21-
test('should render Nested import', async () => {
2215
expect(await getText('#nested #message')).toBe('nested');
2316
expect(await getText('#nested #cjs-and-esm')).toBe('esm');
24-
});
25-
26-
test('should render api-only import', async () => {
2717
expect(await getText('#api-only')).toBe('api loaded: true');
28-
});
18+
expect(await getText('#simple .label')).toBe('dependency-import');
19+
expect(await getText('#exports-simple .label')).toBe('dependency-import');
20+
}
2921

3022
if (!isBuild) {
23+
test('page works with pre-bundling enabled', async () => {
24+
await expectPageToWork();
25+
});
3126
test('should optimize svelte dependencies', () => {
3227
const metadataFile = readVitePrebundleMetadata();
3328
const metadata = JSON.parse(metadataFile);
@@ -46,4 +41,30 @@ if (!isBuild) {
4641
expect(optimizedPaths).not.toContain('e2e-test-dep-svelte-hybrid');
4742
expect(optimizedPaths).toContain('e2e-test-dep-svelte-hybrid > e2e-test-dep-cjs-only');
4843
});
44+
45+
test('page works with pre-bundling disabled', async () => {
46+
editFile('svelte.config.js', (c) =>
47+
c.replace('prebundleSvelteLibraries: true', 'prebundleSvelteLibraries: false')
48+
);
49+
await waitForServerRestartAndReloadPage();
50+
await expectPageToWork();
51+
const metadataFile = readVitePrebundleMetadata();
52+
const metadata = JSON.parse(metadataFile);
53+
const optimizedPaths = Object.keys(metadata.optimized);
54+
expect(optimizedPaths).not.toContain('e2e-test-dep-svelte-simple');
55+
expect(optimizedPaths).not.toContain('e2e-test-dep-svelte-hybrid');
56+
57+
// this is a bit surprising, we always include js-libraries using svelte
58+
expect(optimizedPaths).toContain('e2e-test-dep-svelte-api-only');
59+
60+
expect(optimizedPaths).toContain('e2e-test-dep-svelte-hybrid > e2e-test-dep-cjs-only');
61+
expect(optimizedPaths).toContain('e2e-test-dep-svelte-simple > e2e-test-dep-cjs-only');
62+
expect(optimizedPaths).toContain(
63+
'e2e-test-dep-svelte-nested > e2e-test-dep-svelte-simple > e2e-test-dep-cjs-only'
64+
);
65+
});
66+
} else {
67+
test('page works', async () => {
68+
await expectPageToWork();
69+
});
4970
}

packages/e2e-tests/prebundle-svelte-deps/vite.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,13 @@ export default defineConfig({
1616
// make build faster by skipping transforms and minification
1717
target: 'esnext',
1818
minify: false
19+
},
20+
server: {
21+
watch: {
22+
// During tests we edit the files too fast and sometimes chokidar
23+
// misses change events, so enforce polling for consistency
24+
usePolling: true,
25+
interval: 100
26+
}
1927
}
2028
});

packages/vite-plugin-svelte/src/utils/options.ts

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,19 @@ export async function preResolveOptions(
135135
...viteUserConfig,
136136
root: resolveViteRoot(viteUserConfig)
137137
};
138+
const isBuild = viteEnv.command === 'build';
138139
const defaultOptions: Partial<Options> = {
139140
extensions: ['.svelte'],
140-
emitCss: true
141+
emitCss: true,
142+
prebundleSvelteLibraries: !isBuild
141143
};
142144
const svelteConfig = convertPluginOptions(
143145
await loadSvelteConfig(viteConfigWithResolvedRoot, inlineOptions)
144146
);
145147

146148
const extraOptions: Partial<PreResolvedOptions> = {
147149
root: viteConfigWithResolvedRoot.root!,
148-
isBuild: viteEnv.command === 'build',
150+
isBuild,
149151
isServe: viteEnv.command === 'serve',
150152
isDebug: process.env.DEBUG != null
151153
};
@@ -373,12 +375,17 @@ export async function buildExtraViteConfig(
373375

374376
// handle prebundling for svelte files
375377
if (options.prebundleSvelteLibraries) {
376-
extraViteConfig.optimizeDeps.extensions = options.extensions ?? ['.svelte'];
377-
// Add esbuild plugin to prebundle Svelte files.
378-
// Currently a placeholder as more information is needed after Vite config is resolved,
379-
// the real Svelte plugin is added in `patchResolvedViteConfig()`
380-
extraViteConfig.optimizeDeps.esbuildOptions = {
381-
plugins: [{ name: facadeEsbuildSveltePluginName, setup: () => {} }]
378+
extraViteConfig.optimizeDeps = {
379+
...extraViteConfig.optimizeDeps,
380+
// Experimental Vite API to allow these extensions to be scanned and prebundled
381+
// @ts-ignore
382+
extensions: options.extensions ?? ['.svelte'],
383+
// Add esbuild plugin to prebundle Svelte files.
384+
// Currently a placeholder as more information is needed after Vite config is resolved,
385+
// the real Svelte plugin is added in `patchResolvedViteConfig()`
386+
esbuildOptions: {
387+
plugins: [{ name: facadeEsbuildSveltePluginName, setup: () => {} }]
388+
}
382389
};
383390
}
384391

@@ -392,9 +399,44 @@ export async function buildExtraViteConfig(
392399
log.debug('enabling "experimental.hmrPartialAccept" in vite config');
393400
extraViteConfig.experimental = { hmrPartialAccept: true };
394401
}
402+
validateViteConfig(extraViteConfig, config, options);
395403
return extraViteConfig;
396404
}
397405

406+
function validateViteConfig(
407+
extraViteConfig: Partial<UserConfig>,
408+
config: UserConfig,
409+
options: PreResolvedOptions
410+
) {
411+
const { prebundleSvelteLibraries, isBuild } = options;
412+
if (prebundleSvelteLibraries) {
413+
const isEnabled = (option: 'dev' | 'build' | boolean) =>
414+
option !== true && option !== (isBuild ? 'build' : 'dev');
415+
const logWarning = (name: string, value: 'dev' | 'build' | boolean, recommendation: string) =>
416+
log.warn.once(
417+
`Incompatible options: \`prebundleSvelteLibraries: true\` and vite \`${name}: ${JSON.stringify(
418+
value
419+
)}\` ${isBuild ? 'during build.' : '.'} ${recommendation}`
420+
);
421+
const viteOptimizeDepsDisabled = config.optimizeDeps?.disabled ?? 'build'; // fall back to vite default
422+
const isOptimizeDepsEnabled = isEnabled(viteOptimizeDepsDisabled);
423+
if (!isBuild && !isOptimizeDepsEnabled) {
424+
logWarning(
425+
'optimizeDeps.disabled',
426+
viteOptimizeDepsDisabled,
427+
'Forcing `optimizeDeps.disabled: "build"`. Disable prebundleSvelteLibraries or update your vite config to enable optimizeDeps during dev.'
428+
);
429+
extraViteConfig.optimizeDeps!.disabled = 'build';
430+
} else if (isBuild && isOptimizeDepsEnabled) {
431+
logWarning(
432+
'optimizeDeps.disabled',
433+
viteOptimizeDepsDisabled,
434+
'Disable optimizeDeps or prebundleSvelteLibraries for build if you experience errors.'
435+
);
436+
}
437+
}
438+
}
439+
398440
async function buildExtraConfigForDependencies(options: PreResolvedOptions, config: UserConfig) {
399441
// extra handling for svelte dependencies in the project
400442
const depsConfig = await crawlFrameworkPkgs({
@@ -576,9 +618,11 @@ export interface PluginOptions {
576618
disableDependencyReinclusion?: boolean | string[];
577619

578620
/**
579-
* Force Vite to pre-bundle Svelte libraries
621+
* Enable support for Vite's dependency optimization to prebundle Svelte libraries.
580622
*
581-
* @default false
623+
* To disable prebundling for a specific library, add it to `optimizeDeps.exclude`.
624+
*
625+
* @default true for dev, false for build
582626
*/
583627
prebundleSvelteLibraries?: boolean;
584628

0 commit comments

Comments
 (0)