Hacktoberfest 2026: as issues que os mantenedores marcaram para outubro, abertas e boas para iniciantes. Ver issues do Hacktoberfest

[3.0.0-next.50 / next] Dev with a host-owned SSR environment: the first server-function call after startup, a restart or a new "use server" file waits a fixed 1 s

Aberta
#427 0 comentários 0 reações 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 1 dia

Ninguém assumiu esta issue ainda.

Avaliação

Dificuldade
3/5
Tempo estimado
1-2 dias
Facilidade para iniciantes
58/100
Tipo de issue
Bug
Clareza
Claramente especificada
Status de atividade
Ativa
Stack de tecnologia
typescript, vite
Domínio
backend, devtools

Direção de pesquisa

Start in src/server-functions/index.ts, where the issue says the manifest load always waits on a 1,000 ms debouncer. Read the manifest load function and the dev invalidation path, then compare the behavior with the proposed change that returns the current module list outside builds. Verify that development loads return promptly while build loads still use the debouncer; the report includes reproduction timings and cases for new or client-only functions.

Escrita pelo modelo de indexação a partir do texto da issue.

Descrição

Summary

In dev, when another plugin owns the SSR environment (@cloudflare/vite-plugin with viteEnvironment: { name: "ssr" }), the generated server-function handler imports virtual:solid-server-function-manifest. Every load of that module waits a fixed 1,000 ms debounce. Each time a transform registers a module the manifest doesn't list yet, it reloads, so the next request waits a full second. That request is the first call after a new "use server" file is added. It is also the first server-function call after every dev-server start or restart, when the function's module is only compiled after startup (for example, one used only in event handlers). Startup and restart also take a second longer. With the plugin's own SSR environment none of this happens. The debounce is only needed for builds.

Reproduction

@solidjs/vite-plugin 3.0.0-next.50, solid-js and @solidjs/web 2.0.0-rc.15, @cloudflare/vite-plugin 1.64.0, wrangler 4.151.0, Vite 8.3.4, Node 24.21.0, Linux.

// package.json
{
  "private": true,
  "type": "module",
  "dependencies": { "@solidjs/web": "2.0.0-rc.15", "solid-js": "2.0.0-rc.15" },
  "devDependencies": {
    "@cloudflare/vite-plugin": "1.64.0",
    "@solidjs/vite-plugin": "3.0.0-next.50",
    "vite": "8.3.4",
    "wrangler": "4.151.0"
  }
}
// vite.config.ts
import { cloudflare } from "@cloudflare/vite-plugin";
import solid from "@solidjs/vite-plugin";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    cloudflare({ viteEnvironment: { name: "ssr" } }),
    solid({ start: true, ssr: true, serverFunctions: true }),
  ],
});
// wrangler.jsonc
{
  "name": "repro",
  "main": "src/worker.ts",
  "compatibility_date": "2026-09-19",
  "compatibility_flags": ["nodejs_compat"],
}
// src/worker.ts
export { default } from "virtual:solid-ssr-handler";
// src/greet.ts
"use server";

export async function greet(name: string) {
  return `hello ${name}`;
}
// src/App.tsx
import { createSignal, Show } from "solid-js";
import { greet } from "./greet";

export default function App() {
  const [message, setMessage] = createSignal<string>();
  return (
    <main>
      <h1>repro</h1>
      <button id="call" type="button" onClick={async () => setMessage(await greet("world"))}>
        call
      </button>
      <Show when={message()}>{(text) => <p id="out">{text()}</p>}</Show>
    </main>
  );
}

Install with pnpm, run DEBUG=vite:load pnpm exec vite, open the page, and click call. The manifest load takes a second before ready, and again at the first call, just before the server first loads greet.ts:

vite:load 1000.51ms [plugin] virtual:solid-server-function-manifest   (startup)
  VITE v8.3.4  ready in 2688 ms
vite:load 1000.66ms [plugin] virtual:solid-server-function-manifest   (first call)
vite:load 0.60ms [fs] /src/greet.ts

Measured in headless Chromium on a fresh dev server with a warm dependency cache. Each cell is the median of 3 runs, all taken from the same 3 runs per row:

Setup Startup First page First call Second call New function
Host-owned SSR 3,215 ms 38 ms 1,085 ms 29 ms 1,058 ms
+ warmup 2,713 ms 1,065 ms 34 ms 22 ms 1,051 ms
+ the fix 1,952 ms 38 ms 71 ms 31 ms 57 ms
Plugin's own SSR 302 ms 365 ms 45 ms 5 ms 11 ms
  • Setup:
    • Host-owned SSR is the configuration above: @cloudflare/vite-plugin owns the SSR environment.
    • + warmup adds environments.ssr.dev.warmup: ["./src/**/*.ts"] to it.
    • + the fix applies the change under Possible fix to it.
    • Plugin's own SSR is the same project without cloudflare().
  • Startup: Vite's ready in. It varies by a few hundred ms between runs. Over every run, the medians are 2,776 ms unpatched (13 runs) and 1,827 ms with the fix (18 runs). The startup manifest load itself took 1,000 ms in every unpatched run, and under 10 ms with the fix.
  • First page: the time until the server starts answering the first page request, made right after ready. With warmup, this request is the one that waits the second.
  • First call: from the click on call to the end of the server-function response.
  • Second call: the same after reloading the page.
  • New function: on that page, save a new src/shout.ts ("use server", export async function shout(name)), then save App.tsx with a button that calls it. Once the page shows the change through HMR (no reload) and the network is idle, time the click on the new button.

Other edits on the same page didn't wait in any row: a second function added to greet.ts, a change to greet's body, and renaming greet (53–77 ms; 4–6 ms with the plugin's own environment). Those modules were already listed in the manifest.

More cases, medians of 3:

Case Host-owned SSR + warmup + the fix Plugin's own SSR
Restart 1,835 ms 1,732 ms 1,012 ms not run
Call after restart 1,060 ms 30 ms 65 ms not run
Client-only module 1,066 ms not run 84 ms 23 ms
Outside warmup's glob 1,090 ms (1 run) 1,090 ms 76 ms not run
  • Restart: from saving vite.config.ts to Vite's server restarted.
  • Call after restart: the first call after that restart. With warmup the call is fast, but the page's automatic reload after the restart took 1,114 ms.
  • Client-only module: the first call to a function only the client loads (const { ping } = await import("./ping") in the click handler).
  • Outside warmup's glob: the same with the function in Ping.tsx (function-level "use server"), which ./src/**/*.ts doesn't match.

The wait delays the call itself. With @solidjs/diagnostics installed, the click below is reported as LONG_HOLD (warn) in 3 of 3 unpatched runs, held 1,067, 1,080 and 1,483 ms. The capture's call record for greet took 1,051–1,389 ms. There was no LONG_HOLD in 3 runs with the fix (calls 55–147 ms) or in 3 with the plugin's own environment:

const [name, setName] = createSignal<string>();
const message = createMemo(async () => {
  const value = name();
  if (!value) return "idle";
  const { greet } = await import("./greet");
  return greet(value);
});
// <button onClick={() => setName("world")}>, and under <Loading>:
// <Show when={isPending(() => message())}><p>updating</p></Show>
// <p>{message()}</p>
Cause

src/server-functions/index.ts (identical on next and at 3.0.0-next.50). The generated handler imports the manifest in dev only when the plugin can't run the SSR environment itself:

const externalDev =
  this.environment.mode === "dev" &&
  (internal.externalDevServer || !isRunnableEnvironment(this.environment));
return handlerModuleCode(isBuild || externalDev);

With its own environment, the dev middleware imports the called function's module directly and never loads the manifest. The manifest's load always awaits a Debouncer, which resolves 1,000 ms after the last defer():

// The manifest can only be emitted once every module has been transformed
// (each transform may register new entries), but Vite gives no such signal —
// so the manifest load resolves a debounced snapshot that transforms keep
// pushing back while they are still landing.
class Debouncer<T> {
  // ...
    this.timeout = setTimeout(() => {
      this.promise.resolve(this.source());
    }, 1000);
const current = new Debouncer(() =>
  [...manifest.modules[mode]].map((entry) => `import ${JSON.stringify(entry)};`).join("\n"),
);
preload[mode] = current;
const result = await current.promise.reference;
return result;

The host evaluates the server entry while the dev server starts, so ready waits for the first load. When a transform first sees a module with server functions, it invalidates the manifest, for client and server transforms alike:

const preloader = preload[mode];
if (preloader) preloader.defer();
invalidateModules(
  currentServer,
  mergeManifestRecord(manifest.modules.server, new Set([id])),
  manifestId,
);

The next request that reaches the server entry imports the manifest again and waits another second. In the repro, the server render never compiles greet.ts, because it's only used in a click handler. The browser's request for it registers it, so the first call is that next request. When the server render calls greet itself (from a createMemo), the startup render compiles greet.ts during the first load's debounce. Then the first call doesn't wait (36 ms, median of 3), but startup still does.

Why the debounce isn't needed in dev

The debounce is for builds. Vite gives no signal that all transforms are done, and the emitted manifest must list every module. In dev, each transform that registers a module already invalidates the manifest, as above. A browser can only call a function after it has loaded the function's module, and that module's client transform registers it. So the module is registered, and the manifest invalidated, before any call can reach it. The next import of the manifest returns the current list. Each wait in the tables is a reload after such an invalidation, not a missing entry.

Why warmup isn't enough

environments.ssr.dev.warmup, with a glob covering every server-function file, makes the first call fast. But it races the debounce rather than removing it:

  • The startup load still takes 1,000 ms.
  • The modules it compiles invalidate the manifest, so the first page request waits a second instead (TTFB 1,065 ms, against 38 ms without warmup).
  • The same happens after a restart.
  • A "use server" file added later still waits (1,051 ms), and so does a function outside the glob (1,090 ms).
  • It only helps users who know to add it and keep the glob complete. The default config always waits.
  • It also compiles everything in the glob at startup. That cost wasn't noticeable in this small repro.
Expected

The first call costs about what later calls do, as with the plugin's own SSR environment. Startup and restarts don't wait a fixed second.

Possible fix

In the manifest's load, return the current list immediately outside builds, and keep the Debouncer for builds:

const source = () =>
  [...manifest.modules[mode]].map((entry) => `import ${JSON.stringify(entry)};`).join("\n");
if (!isBuild) return source();
const current = new Debouncer(source);

Tested by applying this change to the installed 3.0.0-next.50 dist/esm/index.mjs and dist/cjs/index.cjs, with the repro above:

  • Timings: the "+ the fix" row of the first table and column of the second. Every manifest load took under 10 ms.
  • A function added while the server runs: callable (57 ms). So were a function added to an existing file (54 ms), a changed body (69 ms) and a renamed function under its new name (60 ms).
  • A function in a module only the client has compiled: works (84 ms).
  • A renamed function's old address: keeps answering until the server restarts. That was the same without the change and with the plugin's own environment. After a restart, it answers 404 Unknown server function. The change doesn't affect what is reachable.
  • vite build: the change doesn't touch it, because isBuild is true when the manifest loads during the build, which still waits for the debounce. With a module function, a function-level "use server" in App.tsx and a module that is only imported dynamically, dist/server/server.js and the client build's .vite/solid-server-functions.json listed all three in 5 builds without the change and 4 with it. Their order varied between builds either way.

(submitted by Claude Opus 5.5 on behalf of rvlzzr)

Linguagem predominante
TypeScript
Estrelas
522
Forks
73
Merge médio
1d 4h
PRs com merge (30d)
35

Preparar o ambiente

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de solidjs/solid-vite-plugin

Todas as issues de solidjs/solid-vite-plugin

Issues semelhantes

Mais issues de TypeScript

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.