An experiment testing React + styling methods' impact on bundle size.
- A styling technique's pros and cons aren't immediately obvious without considering and measuring all aspects of an app--from UX (best estimate via Lighthouse) to SSR performance. Bundle size isn't always an accurate predictor of TTI, for example.
- Splitting styles up per page--recommended by Top 10 performance pitfalls - HTTP 203--is essential. Tachyons, which achieves performance by caching everything, falls short of other solutions on that all-important first page load.
- CSS-in-JS solutions aren't optimized for React SSR compared to more traditional methods.
- A library's size itself isn't the best indicator of performance of an app build with said library. Sometimes actual use of a library results in less app code per feature.
- This exercise was only possible by writing Tachyons first--constraining styles to what was available in Tachyons. Working the other way around would have resulted in inconsisten UI between considered options.
This project contains web applications of the Page component that have server-side rendering through a lightweight fastify server and rehydrate the React application on the client. Running these through Lighthouse's Performance tool (Mobile, average of 10 runs) yields the following numbers:
Score | First Content Paint | Time to Interactive | Speed Index | Total Blocking Time | Largest Contentful Paint | |
---|---|---|---|---|---|---|
CSS Modules | 99 | 1.6s | 2.4s | 1.6s | 97ms | 1.8s |
Emotion | 97 | 1.9s | 2.6s | 1.9s | 120ms | 2.3s |
Inline Styles | 99 | 1.6s | 2.2s | 1.6s | 128ms | 1.7s |
Styletron | 99 | 1.5s | 2.4s | 1.5s | 90ms | 1.8s |
Tachyons | 96 | 2.0s | 2.9s | 2.1s | 154ms | 2.3s |
The comparison script demonstrates:
Button JS | Button CSS | Sidebar JS | Sidebar CSS | Page JS | Page CSS | App (Page + React) | |
---|---|---|---|---|---|---|---|
CSS Modules | 320 | 424 | 861 | 682 | 2047 | 1631 | 44641 |
Emotion | 6482 | 0 | 7355 | 0 | 9269 | 0 | 53781 |
Inline Styles | 789 | 0 | 1607 | 0 | 3675 | 0 | 46180 |
Styletron | 1363 | 0 | 2162 | 0 | 4289 | 0 | 51725 |
Tachyons | 457 | 15558 | 1122 | 15558 | 2530 | 15558 | 45057 |
(All sizes gzipped.)
Using React's server-side rendering reveals differences aren't limited to client bundle sizes:
$ for f in scripts/sync/*.mjs; do NODE_ENV=production node "$f" 2>/dev/null; sleep 2; done
# ...
$ for f in scripts/stream/*.mjs; do NODE_ENV=production node "$f" 2>/dev/null; sleep 2; done
# ...
renderToString |
renderToNodeStream |
HTML bytes (gzip) | |
---|---|---|---|
CSS Modules | 1093.28 ops/sec | 848.76 ops/sec | 5190 |
Emotion | 204.07 ops/sec | 86.87 ops/sec | 8055 |
Inline Styles | 507.81 ops/sec | 385.94 ops/sec | 7280 |
Styletron | 255.00 ops/sec | 224.97 ops/sec | 6967 |
Tachyons | 1000.80 ops/sec | 746.11 ops/sec | 6166 |
This test uses two components for testing. Both components are coded separately with CSS Modules and Tachyons, using the same DOM and styles. There's no visual difference in the components.
A simple button with a few properties.
A more complicated navigation component with some state.
A larger component that includes Button and Sidebar along with fake products and some additional content.
-
Make sure Node.js 14.x.x is installed
-
Clone the repository
-
Install dependencies in the repository directory:
npm install
Run the project's Storybook:
npm start
Run the project's build script and compare file sizes:
# Clean build directory
npm run clean
# Build the project
npm run build
# Compare file sizes
npm run compare
Run the project's server to load the web applications in a local browser:
# Build the project
npm run build
# Run the server
node src/server/server.mjs --handler cssmodules --mode sync
Open localhost:3000 to see the web application.