Skip to content

Dev-server request stays pending for as long as the client renderer is frozen — large modules (inline dev sourcemap) hold the response in socket backpressure while vite's own timings stay fast #23407

Description

@wojtekpiskorz

Title: Dev-server request stays pending for as long as the client renderer is frozen — large modules (inline dev sourcemap) hold the response in socket backpressure; vite's own timings stay fast

Summary

On vite 8.2.2 (macOS, Apple Silicon, Node 22, Chromium via Playwright), a dev-server module request can sit "pending" in the browser for a minute or more while every timing inside vite is milliseconds. We chased this as a server-side stall for a long time; it is not one. The delay is the client: when the tab's renderer process gets no CPU (heavy ambient load — or a scheduler hiccup), it stops reading the response socket, the server's res write backpressures, and the request resolves only when the renderer is scheduled again. vite serves the module fine in the meantime — the transform and send complete in ms-to-~2s under load; the finish of the oversized response is what waits.

Repro (deterministic, no ambient luck needed)

Any vite dev server with a genuinely large module. The pain threshold is set by the OS socket buffers: a small module (<~1MB) fully buffers server-side and the request finishes while the renderer is frozen; a multi-MB one does not. Large modules are easy to hit in dev because vite inlines the module's sourcemap as a base64 sourceMappingURL data URL — in our case a 2.2MB prebuilt app bundle serves as 7.85MB (72% inline map).

  1. Serve a multi-MB module in dev (e.g. a prebuilt bundle behind a virtual module).
  2. Load the page; ~400ms in, SIGSTOP the tab's renderer process for 25s (emulates the starvation; Playwright + ps tree walk finds the renderer pid).
  3. Observe: the module request is pending for exactly the freeze (+drain), then completes; a fresh page right after loads fast.

Our measured run: module request 25.8s browser-side / 25.7s server-side finish trace, page marker visible at 26.2s, next fresh page 1.8s. The same shape shows up in the wild as a bimodal flake: healthy boots ~7s vs whole-budget losses (45-105s) with nothing between, "request-scoped" (the next fresh page is fast), scaling with ambient machine load. This matches the closed-unreproduced #16469 ("stuck or slow pending requests" with sub-ms vite debug timings).

Probe script and the full investigation ledger (including the server-side trace showing the transform pipeline at 69ms-1.8s under heavy contention while the response finish was held 25.7s): https://github.com/wojtekpiskorz/astroix/tree/main/e2e/stall-lab (freeze-probe is probe/freeze-probe.mjs; it is astroix-flavored only in the marker selector and the virtual module — the mechanism needs just a large dev module and a page).

What vite could do about it (our read, owner of the above repo)

  • The inline base64 sourcemap is the amplifier: it tripled our wire bytes with a map that maps transformed output back onto a prebuilt bundle (no debugging value). Options we saw: serve dev sourcemaps as a separate request rather than inline above some size, or let a plugin opt a module out of the inline map (today the off-switch is returning a version-less { mappings: '' } sentinel from the last transform — undocumented but load-bearing; a versioned partial map crashes _getCombinedSourcemap with Cannot read properties of undefined (reading 'length')).
  • If nothing changes vite-side, the behavior is survivable downstream (we shrink the payload ourselves and keep a boot-budget-with-reload gate in CI) — but the pending-request signature misleads debugging toward the server, which is why we think it's worth an upstream note.

Happy to provide a minimal vanilla-vite repro (large generated module + the freeze probe) if useful.

Activity

  1. arpitbharadwaj1 commented on Sep 2, 2026

    @arpitbharadwaj1

    Thanks for the exceptionally thorough writeup — the freeze-probe methodology (SIGSTOP the
    renderer, compare server-side finish trace against browser-side pending time) is a genuinely
    useful way to falsify the "server is slow" hypothesis, and I think you've correctly identified
    the mechanism.

    I traced this against current main to confirm before commenting:

    send() (packages/vite/src/node/server/send.ts) is the single path every JS/CSS dev
    transform response goes through, and when a map is present it unconditionally inlines it via
    getCodeWithSourcemap() → genSourceMapUrl() — a base64 JSON.stringify of the whole map,
    appended directly onto content before res.end(content). There's no size check anywhere on
    that path, and I couldn't find any existing backpressure handling in the dev server for this
    case. So yes: the inflated bytes are structurally part of the one response already committed
    to res.end(), not something that could be cancelled or deferred independently once socket
    backpressure kicks in on a stalled client.

    You're also right that { mappings: '' } is a real, load-bearing sentinel — it's what
    send()'s inlining gate checks (map.mappings truthy), and _getCombinedSourcemap() in
    pluginContainer.ts special-cases it through the whole sourcemap chain. It shows up in several
    other files (transformRequest.ts, css.ts, worker.ts, esbuild.ts, asset.ts, html.ts) as internal
    plumbing, but I couldn't find it documented as a public plugin contract anywhere — so "return
    this exact shape to opt out" isn't something a plugin author could discover without reading
    core source, which matches your read.

    On your two options — my instinct is a size threshold before falling back to a separate
    request is the safer default (doesn't change the wire format for the common case, and the
    compat risk of "some tooling reads inline maps and can't handle a separate request" only shows
    up above the threshold, which by definition is already the pathological case). A documented
    plugin-facing opt-out (rather than the implicit { mappings: '' } shape) seems like a good
    complement regardless, since it's cheap and unblocks people who already know their module
    doesn't need inline debugging info.

    Given this touches the response path for every dev-server module and CSS request, I'd rather
    get a maintainer read on the direction (threshold value, config surface, whether "separate
    request" should mean a ?sourcemap sub-request under the same URL or something else) before
    putting up a PR. Happy to build the minimal repro/benchmark alongside whichever direction you'd
    prefer, or take a first pass at a threshold-gated version if that's the preferred shape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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