Subscribe on changes!

Manual inline/critical CSS, or CSS groups

avatar
Jul 6th 2021

What problem does this feature solve?

Allowing for performance optimisation by picking which CSS to render into the head of the page during SSR, and which to load later.

It would be great if we could choose to tag css to be compiled into a separate file somehow. We could then add the separate CSS to the head of the page during or after SSR rendering

What does the proposed API look like?

Add two style tags to the SFC file, one with a tag, e.g:

<style critical>
...
</style>
<style>
...
</style>

Add a configuration option to control which tags get split. The goal is to have the build generate two or more CSS files so one of them can be rendered into the head of the page during or after SSR

avatar
Jul 6th 2021

Just realised I'm not entirely sure if this would be in sfc-compiler or vite or both.

avatar
Jul 6th 2021

Introducing something like <style critical> should definitely go through an RFC

avatar
Jul 6th 2021

Please use https://github.com/vuejs/rfcs for ideas like this.

avatar
Jul 7th 2021

If anyone comes across this wanting to do something similar, I created a quick and dirty vite plugin to be placed after the vue plugin. It modifies the SFC code to load the <style critical> CSS into the ssrContext, which can then be retrieved and injected into the html after renderToString

function performanceOptimisationPlugin(): Plugin {
  return {
    name: 'vite:performanceOptimisation',
    transform(code: string, id: string, ssr?: boolean) {
      if (ssr && id.endsWith('.vue') && code.includes('&critical=true')) {
        const matches = code.matchAll(/^import "(.*?&critical=true.*?&lang.css)"$/gm)
        let imports = ''
        const vars = []
        for (const match of matches) {
          imports += (`\nimport criticalCss${match.index} from "${match[1]}"`)
          vars.push('criticalCss'+match.index)
        }
        if (imports) {
          const insert = `\n  ;(ssrContext.criticalCss || (ssrContext.criticalCss = [])).push([${vars.join(', ')}].join('\\n'))`
          code = code.replace("import { useSSRContext as __vite_useSSRContext } from 'vue'", `$&${imports}`)
          code = code.replace('const ssrContext = __vite_useSSRContext()', `$&${insert}`)
          const map = this.getCombinedSourcemap()
          return { code, map }
        }
      }
      return
    },
    transformIndexHtml(html, { bundle }) {
      if (bundle) {
        // If we're building, transform the html to have async JS/CSS
        const $ = cheerio.load(html)
        $('script').attr('async', 'async')
        $('link[rel=stylesheet]').attr('media', 'print').attr('onload', "this.media='all'")
        return $.html()
      }
    }
  }