Library Upgrade Guide: <link rel="stylesheet"> (E.g. custom CSS imports/modules) #108
sebmarkbage
announced in
Announcement
Replies: 1 comment 1 reply
-
In the "Document Order Precedence" section you mention the strategy to move |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Library Upgrade Guide:
<link rel="stylesheet">
This is an upgrade guide for CSS libraries that inject
<link rel="stylesheet">
tags specifically for use with React. In most systems today, that would be things like CSS imports and CSS modules. It might also be CSS-in-JS libraries that generate CSS external files like vanilla-extract (or Facebook's own internal CSS system). These days, those systems aren't React specific and just use the module system/bundler. However, there are often React specific concerns that meta-frameworks have to deal with around SSR and hydration. As we'll see, there might also be reason to make these more React specific in the future.This guide is specifically not for CSS libraries that generate CSS rules on the fly and insert
<style>
tags. E.g. styled-components, styled-jsx, react-native-web etc. See this separate guide for<style>
tags.Injecting Into the SSR Stream
If you extract everything into literally a single CSS file for your whole site you might get away with just putting a
<link>
in the<head>
and call it a day. That might not be such a bad idea for many small sites that use atomic CSS or reusable rules like tailwind. Those tend to plateau in their size growth.However, even in those systems you eventually tend to grow out of them and loading all CSS up front becomes a bad idea. In those cases you want to inject CSS files based on if they're used by the components you're rendering on the server.
If you have lazy components and a fresh module system per request, you can track which CSS modules have been imported on the server. Another technique is to inject something into the component with a compiler to track CSS files. This technique isn't commonly used. Instead, it's more common to track all reachable CSS from some kind of entry point like a Page which lets you split per "page" but it's less precise.
Regardless, you need some way to collect which files were used at runtime on the server and emit them to the SSR stream.
Similar to the
<head>
, this doesn't work for a streaming API. If you only have one CSS file you can probably amend the head once. That doesn't work if you have deeply decided branches.A more generic strategy would be to emit all additionally collected link tags that were discovered since the last time they were written.
To insert into the stream at the right timing, you'll need to provide an intermediate stream.
For Node.js that means creating a wrapper Writable.
Note that it's important to use a custom Writable instead of a Transform stream to support forwarding of the flush() command, to avoid GZIP buffering.
For a Web Streams, there's no support anyway. Browser support is a little flaky but the principle is the same. Find a way to inject before whatever React writes.
Ensure that you've already written the
<!doctype html><html><head>
part before writing any link tags. Otherwise the link tags could be written before the doctype.Note: React can write fractional HTML chunks so it's not safe to always inject HTML anywhere in a write call. The above technique relies on the fact that React won't render anything in between writing. We assume that no more link tags will be collected between fractional writes. It is not safe to write things after React since there can be another write call coming after it.
Document Order Precedence
Your CSS might have conflicting rules and relies on document order to resolve them. In that case you might want to have the first call inject plain link tags and then have subsequent calls inject script tags.
or script tag that moves them:
That way you can stream in CSS files late while still preserving document order. Most files will hopefully be in the first call. So they're all plain HTML. This is what happens if you delay writing the React content until onCompleteAll for SEO purposes.
Selective Hydration
If you're only rendering a single CSS file then maybe you don't have to bother with client's inserting CSS at all and so you don't need to hydrate. However if a client component might in the future insert new CSS files, it's good to avoid inserting duplicates.
Therefore, a common technique is to scan the document for
<link>
tags upon hydration so that you can avoid inserting a duplicate.With streaming partial hydration, hydration can start early before the stream is complete. Therefore there might be more
<link>
tags added. Therefore it might not be enough to scan only once for all link tags. It should be safe to scan once per URL though, since if it gets inserted, it should've already been inserted by the time you're hydrating the content that uses it. If you're following the techniques outlined above.Most systems like these are append-only. In that case you don't even need to scan for the
<link>
tag in the HTML. You can just assume that it's already there, if you're hydrating (we'll add an API to check if you're hydrating), and add the URL to a Map to avoid duplication.There's an edge case, that if you're hydrating and the user triggers some UI that is client rendered - before the hydration has happened or before the stream has sent the last link tag, it's possible that the client render will insert it first. In that case, you might end up with duplicate link tags to the same URL.
In an append-only system that might be fine. Because you're not expected to have conflicts between components anyway.
We don't recommend building a system where you can rely on conflicting rules being resolved by unmounting a previous component. It makes it harder to do animations between two states. However, if you do have a system like that you need to also consider that you might not hydrate everything on the page. With selective hydration, it's possible to delete a Suspense boundary before its content gets hydrated. In that case you wouldn't have scanned the tree to extract it, so it'll be a hanging reference. That's another way you can end up with a duplicate.
Future
This is all very complicated so we want to add a built-in way to render global CSS references to React. One possible idea would be something like:
It would just globally insert a single
<link>
tag for this href no matter how many components render it.The precedence prop would be React specific and determines where in the document order it needs to be.
You wouldn't be expected to write this as an end user. It'd be expected that CSS-in-JS library renders these as part of their APIs.
Why React Specific APIs?
Existing libraries tend to just use the module system for this so the above proposal wouldn't be directly usable by existing APIs. There are some problems with existing APIs though.
There's no way to track which component used which CSS file. This means that you can just send down all the CSS you needed for the whole page, you can't send it in the order of priority of the content that it belongs to.
There's no way to inject CSS dependencies into Server Components since they don't execute in the same module system as the client.
There's no life-cycle associated with modules. They are append-only. Therefore there's no way to unmount the CSS. That might be ok for modules because they just waste memory, but CSS rules needs to be applied to all nodes so as the number of nodes and rules grow, everything gets slower. This might be less of an issue for small sites and large sites tend to eventually reload the whole page anyway (since cleaning up 100% of leaks is hard). It's also bad to clean up too aggressively since if you need it again you cause layout recalc then instead. It's unfortunate not to have the option to clean up periodically though.
A bigger problem with existing APIs is that they block JS from executing. The module system will wait until all CSS has loaded before it lets and JS run. This loses a parallelization opportunity because we could run all the React renders before the CSS has even finished loading, and then display once it's ready.
We think the ideal loading sequence is that when you're targeting client-rendering (as opposed to SSR), we believe that it's ideal to have JS first in the network queue and then later CSS. That way when the JS finishes, you can start rendering even if the CSS is still pending. Meanwhile, if you load the CSS first it can't do anything until the JS finishes.
React can use Suspense for this purpose. We also have similar ideas for images.
All of this can be automatic with a React component centric API design. Therefore it might be time for libraries to also consider that as part of the API design.
Dynamic Insertion
One issue with inserting CSS in general is that each time you insert new rules, you have to reevaluate all rules against all the existing DOM nodes. This is fast for a small site but scales poorly to many rules and many DOM nodes. So a goal is to minimize the number of CSS insertions.
This is a big issue for
<style>
tags.The ideal is to do it synchronously at the same time as DOM mutations. For
<link>
tags this gets tricky because there's no API to insert it synchronously. In some browsers, you can do this with<link rel="preload" />
and then change it tolink.rel = "stylesheet";
synchronously. This doesn't work in all browsers though.Luckily this is much less of an issue for
<link>
tags than<style>
tags because you tend to have much fewer external CSS files. Therefore we recommend quite large CSS files with many rules to avoid dynamic insertion of too many files.Beta Was this translation helpful? Give feedback.
All reactions