Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

feat(nuxt, vite): inline global and component styles in server response #7160

Merged
merged 13 commits into from
Sep 3, 2022

Conversation

danielroe
Copy link
Member

@danielroe danielroe commented Sep 2, 2022

πŸ”— Linked issue

partly resolves nuxt/nuxt#14653

❓ Type of change

  • πŸ“– Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • πŸ‘Œ Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

πŸ“š Description

This PR adds support in Vite (and Nitro) for inlining global + component styles when returning an HTML response. It has the capacity to significantly decrease CLS on initial render.

I tested two other approaches before deciding on this one:

  1. Directly load css emitted by components. Negatively, vite's asset helper doesn't process this so we end up with __VITE_ASSET__ hashes and would have to handle this manually.
  2. Emit CSS within components via ssrContext (as this great POC demonstrates). This was potentially fragile as it relied upon a specific implementation of the vite vue plugin (but an excellent starting point to implement this upstream in vite). Negatively, it wouldn't support global css (that is, imported outside a vue component), and it would increase the size of each component chunk, which would mean that it wouldn't be easy to granularly enabling/disabling rendering of styles (or even so, it would mean that the extra memory usage of loading css strings would be present even when not inlining the css).

This third approach entirely side-steps @vitejs/plugin-vue and emits css files separately. By doing this, we also benefit from rollup code-splitting css files (+ sharing them between chunks), as you can see if you build the test fixture in this PR and inspect the .nuxt/dist/server/styles.mjs. Moreover, we benefit from Vite's dynamic asset paths when rendering CSS with build assets (like fonts, images, etc.)

There are several ways to fine-tune the behaviour:

  1. It can be entirely disabled by setting experimental.renderInlineStyles to false.
  2. A function can be provided to experimental.renderInlineStyles that receives an id and determines whether CSS imports into that file will be inlined. To restrict inlining to vue components only, a function like id => id.includes('.vue') would work.
  3. Finally, on a per-route basis, setting ssrContext.noInlineStyles will disable inline style behaviour. (If it's beneficial, perhaps we could also use Nitro route rules to determine whether to inline styles?)

Performance

In my preliminary tests, the server directory increased by the size of the inlined CSS, as expected, but I could detect no appreciable difference in performance.

- Ξ£ Total size: 3.25 MB (765 kB gzip)
+ Ξ£ Total size: 3.26 MB (772 kB gzip)
- 612 requests in 10.08s, 1.46 MB read
+ 612 requests in 10.07s, 1.62 MB read

Reflections on client- vs server-first approach

This PR adopts the server-first approach, which means it currently cannot render styles that are only emitted in the client-bundle (such as for client-only components). Here are some thoughts on the difference, though I think I agree with @pi0 that server-first approach is a better option:

Server-first approach

This approach treats the server-build as the source of truth and seeks to add CSS hints or information into the server build. So, for example, each component that is rendered on the server could inject its CSS and this could end up inlined.

The benefit of this approach is that it can be quite granular; we can render just the CSS of a specific component, even if it gets inlined in a bigger CSS file on client-side. It also pairs very well with server components.

On the other hand, we might miss out on CSS for client-only components. They are technically not rendered on SSR.

Client-first approach

This approach treats the client manifest as source of truth. We can process the manifest (produced from the client build) to produce imports the server can consume to inline CSS.

The benefit here is that we can ensure the CSS exactly matches between server-inlined CSS and client-loaded.

On the other hand, it's less granular. Vite may generate larger CSS files that contain styles for components that are never used in rendering. Moreover, it won't include server-component CSS when rendering, which was one of the main points of this PR.

πŸ“ Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@danielroe danielroe added enhancement New feature or request 🍰 p2-nice-to-have Priority 2: nothing is broken but it's worth addressing performance labels Sep 2, 2022
@danielroe danielroe requested a review from pi0 September 2, 2022 12:40
@danielroe danielroe self-assigned this Sep 2, 2022
@netlify
Copy link

netlify bot commented Sep 2, 2022

βœ… Deploy Preview for nuxt3-docs canceled.

Name Link
πŸ”¨ Latest commit d2a1e0f
πŸ” Latest deploy log https://app.netlify.com/sites/nuxt3-docs/deploys/63134a5bc3387e0008b744d9

@danielroe danielroe requested a review from antfu September 2, 2022 12:42
Copy link
Member

@pi0 pi0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’―

@pi0 pi0 merged commit de62520 into main Sep 3, 2022
@pi0 pi0 deleted the feat/inline-styles branch September 3, 2022 13:03
@AndrewBogdanovTSS
Copy link

Inlining of the CSS via nuxt config is still broken

import type { NuxtConfig } from '@nuxt/bridge'

// Global CSS: https://go.nuxtjs.dev/config-css
export default [
  // windi preflight
  'virtual:windi-base.css',
  // your stylesheets which override the preflight
  '~/assets/styles/index.pcss',
  // windi extras
  'virtual:windi-components.css',
  'virtual:windi-utilities.css'
] as NuxtConfig['css']
Nuxt project info:

------------------------------
- Operating System: `Windows_NT`
- Node Version:     `v16.14.0`
- Nuxt Version:     `2.16.0-27711384.54e852f`
- Package Manager:  `pnpm@7.11.0`
- Builder:          `webpack`
- User Config:      `alias`, `autoImports`, `bridge`, `build`, `buildModules`, `componen
ts`, `head`, `modules`, `plugins`, `polyfill`, `privateRuntimeConfig`, `publicRuntimeCon
fig`, `pwa`, `router`, `screen`, `storybook`, `svgSprite`, `windicss`, `serverHandlers`,
 `devServerHandlers`- Runtime Modules:  `@nuxtjs/svg-sprite@0.5.2`, `vue-screen/nuxt`   
- Build Modules:    `()`, `nuxt-windicss@2.5.1`, `@pinia/nuxt@0.4.2`, `@vueuse/nuxt@9.2.
0`, `@nuxt/bridge@3.0.0-27700335.6dfc006`
------------------------------

Copy link
Member Author

~> nuxt/nuxt#14855

This was referenced Sep 9, 2022
@bhaskarGyan
Copy link

Enabling inline style has a negative impact on FCP, LCP, and Speed Index.

I tried checking the overall page performance after upgrading to RC.10 in two different environments with exactly the same code base with Multiple runs on Webpagetest.
the only difference is setting inlineSSRStyles as true is one and false in other env.

The overall perf was better in the environment with inlineSSRStyles:false

RC.10 with inlineSSRStyles:false

Screenshot 2022-09-19 at 7 58 57 PM

RC.10 with inlineSSRStyles:true

Screenshot 2022-09-19 at 7 58 36 PM

Webpagetest Config

Screenshot 2022-09-19 at 8 13 18 PM

is there any other config I need to set?

@danielroe
Copy link
Member Author

@bhaskarGyan There are a number of follow-on issues from this (most notably nuxt/nuxt#14953). Rather than commenting here, please open a new issue with an example code base where you are detecting negative performance impact, and we can take it into account as we develop this feature.

@AndrewBogdanovTSS
Copy link

AndrewBogdanovTSS commented Sep 25, 2022

The performance effect of inlining global CSS heavily depends on how much of it is inlined. I'm using WindiCSS and the size of generated CSS for my project is about 5Kb since my CSS describes the design system and not actual components styling. Including such an amount in the build output improves not only Lighthouse scores but the actual visual fidelity for the user, fully eliminating FOUC which is even more important than any LH metric, but if you use a different CSS approach that forces you to bundle large CSS chunk (I had cases where people included up to 5MB of global CSS) in such case not inlining it, of course, will be more beneficial since browser won't need to parse through so much styling and your page will load much faster. Long story short both cases have it's application scenarios and Nuxt3/Bridge should cover them same way it was always possible for Nuxt 2 - via the extractCSS option which should be false by default.

Copy link
Member Author

There is an important difference between inlining the styles of components used in the page, and inlining all global CSS. You can track the global CSS question at nuxt/nuxt#14953.

Note that if you have a 5Mb global CSS file it will block the rendering of the page just as much as if it is inlined in the page. The key thing is that the 5Mb isn't included in the JS payload and that we don't duplicate it (e.g. download it twice, once in HTML and once in CSS). That is what the linked issue above is tracking.

Also I would say that extractCSS is not really relevant to the way we are implementing this feature. CSS files remain extracted and there is no CSS content in the client-side JS, even with this feature.

May I request that future comments be placed in the appropriate open issues, or a new one be opened if necessary, so that they aren't missed by being added to a merged pull request πŸ™

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
3.x enhancement New feature or request 🍰 p2-nice-to-have Priority 2: nothing is broken but it's worth addressing performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Component styles are not inlined to the page
4 participants