close

Troubleshooting

Deploy Storybook to a subdirectory or subpath

Since v3.3.3. In v3.3.2 and earlier, the default assetPrefix was '/' (absolute), which required manual configuration for subdirectory deployment.

The Rsbuild builder uses relative asset paths by default — the same as the official Vite (base: './') and webpack (publicPath: '') builders. Storybook can be deployed to any subdirectory (e.g., https://example.com/storybook/) without extra configuration.

Serving static assets from a CDN

Since v3.3.3. In v3.3.2 and earlier, this was not supported.

To serve Storybook's JS/CSS bundles from a separate CDN origin (e.g., https://cdn.example.com/assets/), set output.assetPrefix to the CDN URL:

import { mergeRsbuildConfig } from '@rsbuild/core'

const config: StorybookConfig = {
  // --snip--
  rsbuildFinal: (config) => {
    return mergeRsbuildConfig(config, {
      output: {
        assetPrefix: 'https://cdn.example.com/',
      },
    })
  },
  // --snip--
}

The preview template correctly handles absolute URLs, so JS/CSS bundles will be loaded from the CDN while iframe.html can remain on the origin server. Note that the CDN must serve appropriate CORS headers since the bundles are loaded via ES module imports.

NOTE

This is an improvement over the official webpack5 builder, which does not support CDN asset hosting due to its template always prepending ./ to import paths.

CSS url() references to static assets

In v3.3.2 and earlier, CSS url() worked correctly when served from the root path because the default assetPrefix was '/'. Since v3.3.3, the default changed to '' (relative) for subdirectory support, which introduces this limitation.

When CSS files reference other assets via url() (e.g., background: url('./image.png')), the generated paths may resolve incorrectly in the production build. This happens because Rspack emits url() paths relative to the output root, but the browser resolves them relative to the CSS file's location. This is a known trade-off of using relative paths, and the official Storybook builders have the same limitation.

If your stories rely on CSS url() references, you can switch to absolute paths via output.assetPrefix:

import { mergeRsbuildConfig } from '@rsbuild/core'

const config: StorybookConfig = {
  // --snip--
  rsbuildFinal: (config) => {
    return mergeRsbuildConfig(config, {
      output: {
        assetPrefix: '/',
      },
    })
  },
  // --snip--
}
NOTE

Setting assetPrefix: '/' fixes CSS url() references when served from the root path, but breaks subdirectory deployment. Choose the option that fits your deployment scenario.

Template.toString() shows compiled code in Docs source snippet

When using the CSF 2 Template.bind({}) pattern and Template.toString() to display source code in the Docs addon, the snippet shows bundler-compiled output instead of your original source.

This is expected — Function.prototype.toString() serializes the runtime function, which has already been transformed by the bundler. This is not specific to the Rsbuild builder; the same happens with webpack and Vite.

Storybook recommends using the CSF 3 format with the automatic source snippet feature, or manually providing source code through docs source parameters. See the Storybook Docs documentation for more details.

Build errors from unexpected files

NOTE

This issue mostly affects older Rspack versions. Modern Rspack handles webpackInclude correctly.

If you encounter issues where Rspack bundles files it shouldn't (like Markdown files in unexpected places), you can use rspack.IgnorePlugin to exclude them.

// .storybook/main.js
import path from 'path'
import { mergeRsbuildConfig } from '@rsbuild/core'

export default {
  framework: 'storybook-react-rsbuild',
  async rsbuildFinal(config) {
    return mergeRsbuildConfig(config, {
      tools: {
        rspack: (config, { addRules, appendPlugins, rspack, mergeConfig }) => {
          return mergeConfig(config, {
            plugins: [
              new rspack.IgnorePlugin({
                checkResource: (resource, context) => {
                  const absPathHasExt = path.extname(resource)
                  if (absPathHasExt === '.md') {
                    return true
                  }

                  return false
                },
              }),
            ],
          })
        },
      },
    })
  },
}

MSW integration and lazy compilation

When mockServiceWorker.js is found in one of the staticDirs entries — the convention used by msw-storybook-addon and any hand-rolled MSW setup — the builder automatically disables lazy compilation and logs a warning at startup.

This is necessary because MSW's Service Worker intercepts every fetch from the preview iframe, including the dev-server's lazy-compilation RPC (POST /lazy-compilation-using-). The SW's passthrough re-issues a fresh fetch(clonedRequest) that the dev-server aborts, which can leave stories stuck on a blank iframe during cold loads. The issue is rooted in how the lazy-compilation runtime talks to the dev-server via a stateful RPC channel — not a defect of the builder itself — and also affects the official @storybook/builder-webpack5 when lazy compilation is manually enabled. A related failure mode is discussed upstream in mswjs/msw#834 (SW intercepting dev-server HMR SSE).

Leaving the auto-disable in place is the safest choice. If you never explicitly set lazyCompilation, no action is required on your side.

If your project needs lazy compilation alongside MSW, set lazyCompilation explicitly in your builder options — an explicit value always takes priority over the MSW auto-disable:

// .storybook/main.ts
export default {
  framework: {
    options: {
      builder: {
        // Opt back in — expect occasional blank iframes on cold loads.
        lazyCompilation: { entries: false },
      },
    },
  },
}

When you opt back in, patch public/mockServiceWorker.js to let lazy-compilation RPC bypass MSW. Add this guard at the top of the fetch event listener, before MSW's own logic runs:

// public/mockServiceWorker.js
self.addEventListener('fetch', function (event) {
  // Let Rspack / webpack's lazy-compilation RPC bypass MSW so the
  // dev-server sees the original client request, not a re-issued clone.
  if (event.request.url.includes('/lazy-compilation-using-')) {
    return
  }
  // ...existing MSW handler logic
})

Caveats:

  • msw init regenerates mockServiceWorker.js and will overwrite the patch. Re-apply it after upgrading MSW, or automate via a postinstall script that appends the guard if it is missing.
  • The bypass is scoped strictly to the lazy-compilation path; normal story requests still go through MSW and your handlers behave as before.
  • The substring match covers both the Rspack implementation (same-origin POST /lazy-compilation-using-) and webpack5's (dedicated random-port GET /lazy-compilation-using-<module-path>), so the same guard also works if you ever move to the official webpack5 builder with lazy compilation enabled.

Why do sandboxes use getAbsolutePath to resolve the framework?

See Storybook's FAQ for the explanation.

How can I inspect the Rsbuild or Rspack config used by Storybook?

Rsbuild offers a CLI debug mode. Enable it when running Storybook to dump the generated configuration paths.

In development:

DEBUG=rsbuild storybook dev

In production:

DEBUG=rsbuild storybook build

Check the CLI output to see where the Rsbuild and Rspack configs are written.

Can I use this builder with Rspack directly?

Yes. Rsbuild is built on Rspack, so you can feed your Rspack configuration into the Rsbuild builder. Follow the Rspack integration guide to learn how.