Skip to content

perf(compiler-ssr): render static style attributes at compile time - #16010

Open
danielroe wants to merge 2 commits into
vuejs:mainfrom
danielroe:perf/ssr-static-style
Open

danielroe wants to merge 2 commits into
vuejs:mainfrom
danielroe:perf/ssr-static-style

Conversation

@danielroe

@danielroe danielroe commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

I was profiling what was taking time on rendering SSR for nuxt, and noticed in the fixture I was using that a plain style="color: red" attribute got rendered at runtime on every request because it becomes a constant dynamic binding and we then compile that to be rendered with ssrRenderStyle

this PR instead renders static style attributes on plain elements directly into the open tag at compile time, which significantly speeds up rendering static styles...

what before after delta
100 static sections, 400 inline styles 150.2 µs 3.6 µs -98%
same inside v-for, with an interpolation 132.0 µs 6.4 µs -95%
small benchmark script

to get those numbers, build compiler-ssr (node scripts/build.js compiler-ssr -f cjs) on main and on this branch, copying packages/compiler-ssr/dist/compiler-ssr.cjs.js to before.cjs.js and after.cjs.js in the same folder, build the rest with node scripts/build.js shared reactivity runtime-core runtime-dom compiler-core compiler-dom server-renderer vue -f cjs, then run NODE_ENV=production node bench.cjs .

const path = require('node:path')
const root = path.resolve(process.argv[2])
const vue = require(path.join(root, 'packages/vue'))
const serverRenderer = require(path.join(root, 'packages/vue/server-renderer'))
const req = id => (id === 'vue' ? vue : serverRenderer)

const section = body => `<section style="display: flex; gap: 8px; --accent: #00dc82">
  <h2 style="font-size: 18px; font-weight: 600; margin: 0">Title</h2>
  <p style="color: rgb(100, 116, 139); line-height: 1.5">${body}</p>
  <span style="padding: 2px 6px; border-radius: 4px !important">Tag</span>
</section>`

const templates = {
  'static (100 sections, 400 styles)': `<div>${section('Body').repeat(100)}</div>`,
  'v-for + interpolation (100 sections, 400 styles)': `<div><template v-for="i in 100" :key="i">${section('{{ i }}')}</template></div>`,
}

const median = xs => [...xs].sort((x, y) => x - y)[xs.length >> 1]

async function run(name, template) {
  const load = file => {
    const { code } = require(path.join(root, 'packages/compiler-ssr/dist', file)).compile(template)
    return { code, ssrRender: Function('require', code)(req) }
  }
  const variants = { before: load('before.cjs.js'), after: load('after.cjs.js') }
  const render = v => serverRenderer.renderToString(vue.createSSRApp({ ssrRender: v.ssrRender }))
  const time = async (v, n) => {
    const start = process.hrtime.bigint()
    for (let i = 0; i < n; i++) await render(v)
    return Number(process.hrtime.bigint() - start) / 1e3 / n
  }

  if ((await render(variants.before)) !== (await render(variants.after))) {
    throw new Error('output differs')
  }

  const N = 2000
  const ROUNDS = 10
  for (const v of Object.values(variants)) await time(v, N)
  const res = { before: [], after: [] }
  for (let r = 0; r < ROUNDS; r++) {
    for (const k of r % 2 ? ['after', 'before'] : ['before', 'after']) {
      res[k].push(await time(variants[k], N))
    }
  }
  const b = median(res.before)
  const a = median(res.after)
  console.log(`${name}: before ${b.toFixed(1)} µs, after ${a.toFixed(1)} µs, after/before ${(a / b).toFixed(3)}`)
}

;(async () => {
  for (const [name, template] of Object.entries(templates)) await run(name, template)
})()

Summary by CodeRabbit

  • Bug Fixes
    • Improved server-rendered style handling. Static styles are now serialized correctly and combined with dynamic styles and fallthrough attributes, including styles passed through bindings and components.
    • Corrected style precedence and merging in conditional and transition-based rendering, while preserving other style properties.
    • Improved serialization of whitespace, custom property names, !important, and HTML-sensitive characters.

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 03968be6-6484-4f96-a885-372b144b2b2d
Commits

Reviewing files that changed from the base of the PR and between 0ff253b and 2a139bf.

Files selected for processing: 7
  • packages/compiler-ssr/__tests__/ssrComponent.spec.ts
  • packages/compiler-ssr/__tests__/ssrElement.spec.ts
  • packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts
  • packages/compiler-ssr/src/index.ts
  • packages/compiler-ssr/src/transforms/ssrTransformElement.ts
  • packages/server-renderer/__tests__/render.spec.ts
  • packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Walkthrough

The SSR compiler now processes static style attributes, serializes them, and combines them with dynamic styles and bound or fallthrough props. Compiler snapshots and server-renderer tests cover serialization, style ordering, components, SVG, v-show, and attribute fallthrough.

Changes: SSR Static Style Processing

Layer / File(s) Summary
Style transformation and compiler output
packages/compiler-ssr/src/index.ts, packages/compiler-ssr/src/transforms/ssrTransformElement.ts, packages/compiler-ssr/__tests__/ssrComponent.spec.ts, packages/compiler-ssr/__tests__/ssrElement.spec.ts, packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts
The SSR compiler transforms static style attributes and combines them with dynamic styles and bound props. Compiler snapshots cover serialization, escaping, style merging, component styles, and fallthrough attributes.
Server-rendered style output
packages/server-renderer/__tests__/render.spec.ts, packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts
Server-renderer tests cover static and dynamic style output, style ordering, v-show, components, SVG, and style fallthrough.

Priority: Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Other

Suggested labels: :cake: p2-nice-to-have

Suggested reviewers: sxzz

Merge Risk: Minimal · up to 700ce

Merge Risk: ⚪ Minimal · up to 700ce

No merge-blocking issue is established; the change is ready for normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: rendering static style attributes at compile time in the SSR compiler.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@16010
npm i https://pkg.pr.new/@vue/compiler-core@16010
yarn add https://pkg.pr.new/@vue/compiler-core@16010.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@16010
npm i https://pkg.pr.new/@vue/compiler-dom@16010
yarn add https://pkg.pr.new/@vue/compiler-dom@16010.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@16010
npm i https://pkg.pr.new/@vue/compiler-sfc@16010
yarn add https://pkg.pr.new/@vue/compiler-sfc@16010.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@16010
npm i https://pkg.pr.new/@vue/compiler-ssr@16010
yarn add https://pkg.pr.new/@vue/compiler-ssr@16010.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@16010
npm i https://pkg.pr.new/@vue/reactivity@16010
yarn add https://pkg.pr.new/@vue/reactivity@16010.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@16010
npm i https://pkg.pr.new/@vue/runtime-core@16010
yarn add https://pkg.pr.new/@vue/runtime-core@16010.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@16010
npm i https://pkg.pr.new/@vue/runtime-dom@16010
yarn add https://pkg.pr.new/@vue/runtime-dom@16010.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@16010
npm i https://pkg.pr.new/@vue/server-renderer@16010
yarn add https://pkg.pr.new/@vue/server-renderer@16010.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@16010
npm i https://pkg.pr.new/@vue/shared@16010
yarn add https://pkg.pr.new/@vue/shared@16010.tgz

vue

pnpm add https://pkg.pr.new/vue@16010
npm i https://pkg.pr.new/vue@16010
yarn add https://pkg.pr.new/vue@16010.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@16010
npm i https://pkg.pr.new/@vue/compat@16010
yarn add https://pkg.pr.new/@vue/compat@16010.tgz

commit: 700ceb1

@github-actions

Copy link
Copy Markdown

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 111 kB 42 kB 37.7 kB
vue.global.prod.js 171 kB 62.3 kB 55.3 kB

Usages

Name Size Gzip Brotli
createApp (CAPI only) 52 kB 20 kB 18.4 kB
createApp 60.2 kB 23.2 kB 21.1 kB
createSSRApp 64.9 kB 25.1 kB 22.9 kB
defineCustomElement 66.5 kB 25.1 kB 22.8 kB
overall 74.6 kB 28.4 kB 25.8 kB

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant