Quantifying the Impact of Styled Components on Server Response Times
For the few years I've worked at my current gig, I've operated under the assumption that Styled Components probably wasn't doing us any favors with regard to runtime performance. Conceptually, having to basically run a miniature compiler (parse/transform/serialize) in N components during rendering does not sound fast.
I recently had some time during a hackweek to explore this idea further and see if I could put some real numbers behind it. I'm sharing my work in hopes that it's helpful to other devs evaluating whether to re-platform their approach to CSS.
Unminified Styled Components
To start my journey, I went through my typical Node.js profiling routine: start the app w/ --inspect
, execute some code path a bunch to give the JIT a chance to warm-up, then connect via the Debugging Protocol and capture a CPU trace. Unfortunately, this path wasn't super productive initially because Styled Components only ships minified builds with mangled identifier names to npm
. This can be observed by browsing the published artifacts for the package.
If you've ever tried to understanding a CPU trace of minified JavaScript, you likely already know this is a dead end path. So I set out to build my own unminified copy of Styled Components. The path to get this done was roughly:
- Clone the Styled Components repo from Github
git checkout v5.3.10
to match our app's version- Open
package.json
, find therollup-plugin-flow
entry, and changegithub:probablyup/rollup-plugin-flow#breaking-update-flow-remove-types
togithub:quantizor/rollup-plugin-flow#breaking-update-flow-remove-types
(contributor changed their Github username, dep will 404 without change) - Run
yarn
at the root to setup all workspaces - Open
packages/styled-components/rollup.config.js
and remove all references tominifierPlugin
in the various build configs within the file - Run
yarn build
inpackages/styled-components
. - Unminified copies of the library will be written to
packages/styled-components/dist
At this point, I was able to go into my app's node_modules/styled-components/dist
dir and manually update to my new unminified copy.
Analyzing a Trace
With the boring parts done, we can now capture useful trace data and begin exploring.
Zoomed Out
Below is a screenshot zoomed in to just the portions of the trace that involve rendering React components. I have excluded the portions of the trace that account for the incoming HTTP request and initial data fetches. We can say that React rendering happened between 2560ms (when Next.js calls renderToReadableStream
) and 2810ms timestamps, so it took roughly 250ms to render our component tree.
Now that I know how much time the total tree took to render, it's time to isolate what percentage of this work was Styled Components.
Identify Stacks from Styled Components
To get an aggregate view of the costs of Styled Components, we first need to identify which callstacks in the trace are from the library.
As far as I can tell, the large majority of heavy lifting done by a Styled Component is done during render by the internal useStyledComponentImpl
hook, as seen in the screenshow below:
Once useStyledComponentImpl
was identified as the main source of heavy lifting, it started to stand out visually that a decent chunk of time is probably spent rendering these styles
Collecting Totals
Now that we've identified useStyledComponentImpl
, we can switch over to SpeedScope, a really useful CPU Trace viewer with some additional features not present in Chrome DevTools.
In SpeedScope, we can click on a frame in the timeline and get the totals for time spent in that function and its callees across the entirety of the trace.
According to this data, of the roughly 250ms we spent rendering the component tree, 117ms of this was spent generating and injecting CSS in Styled Components. This means, assuming my logic is right, ~47% of SSR time was spent generating and injecting css from Styled Components.
Conclusion
I feel confident at this point saying "Styled Components drastically decreases the performance of SSR for our React app." Next steps for the day job will include capturing more real Production metrics around the costs of useStyledComponentImpl
so we can compare the costs under load in a real environment vs on a developer laptop.
It's worth noting that all the takeaways from this post assume all my profiling and testing was sound, which is always a challenge when doing Performance optimization work. Please reach out if you see anything inaccurate in this post that needs adjustment and I'll make the appropriate edits.