Skip to content

[Start + rsbuild] CSS emitted into both client and server dist, both URLs leak into SSR'd <head> #7543

@ploskovytskyy

Description

@ploskovytskyy

Which project does this relate to?

Router

Describe the bug

Summary

Using the rsbuild, importing CSS via ?url emits the CSS file twice — once in dist/client/assets/css/ and again in dist/server/static/css/ — and both URLs end up in the SSR-rendered <head>.

  • dev: both requests succeed (rsbuild's dev server serves both paths), but the page loads the same stylesheet twice with two HTTP round-trips.
  • prod: the /static/... URL 404s under the standard srvx --prod -s dist/client … start command, because only dist/client is exposed.

Reproduces with or without rsc.enabled.

After rsbuild build:

dist/client/assets/css/styles..css ✅ byte-identical
dist/server/static/css/styles..css ✅ byte-identical, unused at runtime

Rendered HTML:

<link rel="stylesheet" href="/static/css/styles.<hash>.css" data-precedence="default">
<link rel="stylesheet" href="/assets/css/styles.<hash>.css" data-precedence="default">

Complete minimal reproducer

https://github.com/ploskovytskyy/reproduce-start-rsbuild

Steps to Reproduce the Bug

  1. clone the repo and pnpm install
  2. run pnpm dev:rsbuild
  3. open localhost and check in browser inspector, there are two for styles

Optional:

  1. run pnpm build:rsbuild
  2. run pnpm start - it starts prod server
  3. now in the browser second styles request will fail with 404 since we only expose client assets

Expected behavior

A single <link rel="stylesheet"> in the rendered head, pointing at a file served from the client assets directory in both dev and prod.

Screenshots or Videos

No response

Platform

  • Router / Start Version: [e.g. 1.121.0]
  • OS: [e.g. macOS, Windows, Linux]
  • Browser: [e.g. Chrome, Safari, Firefox]
  • Browser Version: [e.g. 91.1]
  • Bundler: [e.g. vite]
  • Bundler Version: [e.g. 7.0.0]

Additional context

Root cause

createRsbuildEnvironmentPlan in packages/start-plugin-core/src/rsbuild/planning.ts defines two independent Rspack compilations:

Client env (lines 121-138) explicitly sets distPath.{css, cssAsync, svg, font, wasm, image, media, assets} under RSBUILD_CLIENT_ASSETS_DIR and assetPrefix: opts.publicBase.

Server env (lines 168-177) sets only distPath.root. Everything else falls back to rspack defaults — cssFilename = static/css/[name].[contenthash:8].css and no assetPrefix.

Each compilation runs its own CSS extract pipeline and produces its own ?url resolution. Since the source CSS is identical, the content hashes match, but the URLs differ (/assets/css/... vs /static/css/...). SSR picks up the server URL; client hydration produces a fresh for the client URL.

This is rspack-specific. Affects any asset imported via ?url in code that runs on both server and client (CSS, SVG, fonts, images). CSS is just the most visible because of the broken in prod and the doubled payload in dev.

Workaround

A top-level output.distPath override cascades into both environments and aligns the URLs:

// rsbuild.config.ts
export default defineConfig({
  output: {
    distPath: {
      css: "assets/css",
      // cssAsync: "assets/css/async", // if you use async CSS chunks
    },
  },
  plugins: [pluginReact(), tanstackStart()],
});

The duplicate file still gets emitted in dist/server/assets/css/ (unused), but the SSR'd URL and the client URL match.

Proposed fix

Apply the same mirroring inside createRsbuildEnvironmentPlan's server env.
Leaves the duplicate emit on disk but removes the visible bug.

For example, here is my branch with the patch and a draft commit: https://github.com/ploskovytskyy/router/tree/fix/start-rsbuild-client-assets-duplicate

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions