Skip to content

resolve svelte to a different module in SSR mode #6372

Closed
@Rich-Harris

Description

@Rich-Harris

Is your feature request related to a problem? Please describe.
In SSR mode, onMount, beforeUpdate and afterUpdate don't do anything. But build tools can't know that because they're not no-ops in the () => {} sense — the callbacks get pushed to an array which is then discarded, which isn't something that can be optimised away with static analysis — and as a result the code remains in the generated bundle.

If onMount etc were instead no-ops, the code could be removed, at least by tools like Rollup and Terser (the same isn't true for esbuild, sadly, but maybe one day).

This won't prevent Rollup from attempting to bundle (or create chunks for) dynamic imports inside those callbacks...

onMount(async () => {
  const mod = await import('./client-only-module.js');
  // client-only-module will appear in the SSR bundle even if `onMount` is correctly
  // treated as a no-op. this feels like something that could change though
});

...but it's nonetheless a significant improvement on the status quo.

Describe the solution you'd like
The solution is a two-parter:

  1. Expose an equivalent module to svelte (e.g. svelte/ssr) that re-exports most stuff from svelte, but replaces the relevant functions with no-ops
  2. Update Svelte bundler plugins like rollup-plugin-svelte and @sveltejs/vite-plugin-svelte to intercept imports to svelte and replace them with svelte/ssr. (This needs to happen for all modules in the graph, not just .svelte files.)

Describe alternatives you've considered
One alternative we discussed is detecting calls to onMount and similar functions, and simply removing them from the generated code. I'm opposed to this for Zalgo reasons: since this would only apply to onMount calls (and only inside components, since Svelte plugins have no authority to mess around with non-components, especially since they may not have been transformed to standard JS by the time the plugin sees them), it would fail to remove code like the following:

// lifecycle.js
import { onMount } from 'svelte';

export function useSomeLibrary(fn) {
  onMount(() => {
    const mod = await import('some-library');
    fn(mod);
  });
}
<!-- App.svelte -->
<script>
  import { useSomeLibrary } from './lifecycle.js';

  let div;

  useSomeLibrary(lib => {
    lib.init(div);
  });
</script>

<div bind:this={div}></div>

If that code is going to fail (because a build tool won't allow some-library to be bundled for the server, for example), it's better that it fails the same way whether you write it inside onMount in a component directly, or in the form shown above. Someone writing the onMount version would be horribly confused if they refactored it into useSomeLibrary to use in multiple components. Better to have more frequent failures than less frequent but unpredictable ones that cause people to lose faith in their understanding of the system as a whole.

How important is this feature to you?
It's come up in the context of SvelteKit, where building fails in some cases because of this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions