A while back, I came across two awesome articles that introduced CSS in JS or the technique of styling your JS components using JS alone: 2017 frameworks and CSS in JS Battle.
Since that time, the popularity of CSS in JS has only grown! It's not hard to see why. With the advent of modern component-driven frontend frameworks, major improvements to stylesheet-driven styling (whether .scss
, .css
, or another styling language) have been to improve the modularity, reduce I/O, ease deduplication of classnames/styles, improve readability, and/or otherwise ease UI development:
I want to briefly introduce CSS in JS, why it matters, and then provide an update comparison using the latest specs.
In-Style Inline Styles
The advent of component-driven client architectures has shown the limitations of existing CSS archetypes (inline
and external stylesheet
).
<style>
//I'm so in
</style>
Inline styling is advantageous since it allows all of our styling to be included in one file (a view or partial view of one kind or another) but becomes unreadable and cumbersome in larger apps. While inline styling reduces I/O by condensing the actual number of files, it's harder to manually deduplicate unnecessarily repeated styles or classes decreasing code reuse and magnifying the actual size of the styling.
In component-driven design, we might run into styling conflict scenarios where, due to complex component layout, the load order or dependency priority can cause some styles to override others in ways we did not want nor anticipate.
Moral of the Story: Can we put styles into the same file as our markup?
External Styling
External styling was originally intended to solve many of the issues raised above. Namely, external styling improves code reuse by allowing the same classes and styles to be efficiently reused throughout an application without multiple style references being used (avoiding duplication).
<link rel="stylesheet" href="external.sheet.css" type="text/css">
Unfortunately, external styling likewise becomes cumbersome in a component-driven client architecture. Full .css
files were originally intended to provided styling for an entire page of a multi-page application. With the advent of both the component-based along with single-page design patterns, such intentions (while well-intentioned) have become somewhat dated.
For example, importing .scss
files in a single-page React app makes all of the imported styling global - e.g., the imported stylesheets are not scoped to the component but to the entire page. This can cause styling conflict scenarios and can become unmanageable in large applications.
Moral of the Story: Can we get both deduplication AND scoped styling?
Programmatic Styling to the Rescue?
var el = document.getElementById('hey');
el.style.height = 10000000%;
A third approach, programmatic-styling, has become more popular given the gradual enrichment of the native JavaScript Web and DOM API's. However, many common but advanced styling techniques are either not available or require complicated scripting to implement well. Furthermore, programmatic-styling moves styling computations out of the CSS interpreter into the runtime JavaScript interpreter which can massively reduce performance.
Moral of the Story: Maybe there's something to this JavaScript...
I CSS Your JS
The CSS in JS paradigm solves these problems by scoping styling to a specific JavaScript component using JavaScript but then injects said styles into the rendered and serve view!
Why CSS In My JS?
What all that means, in theory, is that we can get every awesome benefit of the preceding styling paradigms in a highly-performant way.
The aim of this article is to see what that theory amounts to in practice and how well different attempts to implement such approaches compare to each other!
Performance Testing
Let's set up a few tests for the most popular CSS in JS frameworks!
Aphrodite
CXS
Emotion
Glamorous
JSS
Radium
Styled-Components
Styletron
Specifically, we'll be adding these dependencies and versions into our test suite:
"aphrodite": "=2.1.1",
"cxs": "=6.2.0",
"emotion": "=9.1.1",
"glamor": "=2.20.40",
"glamorous": "=4.12.3",
"jss": "=9.8.1",
"radium": "=0.24.0",
"styled-components": "=3.2.5",
"styletron": "=3.0.4"
If you know of any others, send me an email at adam.gerard@x-team.com, and I'll happily add them to the test suite :)
Performance Tests
We'll approach testing from five vantage points:
- Phantom.js page load performance
- Phantom.js I/0 performance
- live-server 'http' request performance - time to complete 1000 requests
- Webpack 3.6 compile time
- Webpack 3.6 bundle size
These metrics will give us a better grasp of overall client-server interperformance. We'll need two dependencies that we can add to our package.json
:
"live-server": "^1.2.0",
"request": "=2.85.0",
"phantom": "=4.0.12"
live-server is, in my opinion, one of the best test-server libraries since it uses a simple configuration object:
const liveServer = require('live-server')
const opts = {
host: '127.0.0.1',
port: port,
root: `./public/${subroot}`,
file: 'index.html',
wait: 0,
open: true,
logLevel: 1
}
liveServer.start(opts)
live-server supports hot-reloading, varying log-levels, manually-set ports and hostnames, etc. For our present purposes, it's one of the easiest libraries to use among many alternatives like Webdriver.
Since it supports easy startup and shutdown, we can partly simplify some of the architecture for our tests. We can also use the native Node http request
object to realistically capture performance, load, and use. Specifically, we'll leverage live-server to be the workhorse of our test-suite.
Phantom.js is one of the best overall testing frameworks out there. We'll use it to primarily test page load and page rendering speed. For our testing purposes, we'll use the Node wrapper for Phantom.js (npm i phantom
).
Project Architecture
In order to really dig in, we'll need to replicate a development
and a production
setup for each of the frameworks.
We will thus have eight (8) entirely separate React apps supported by distinct Webpack configuration files. Woot!
Our Webpack package.json
dependencies will look like:
"autoprefixer": "=7.2.2",
"babel-core": "=6.26.0",
"babel-loader": "=7.1.4",
"babel-preset-es2015": "=6.24.1",
"babel-preset-react": "=6.24.1",
"babel-preset-stage-0": "=6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"css-loader": "=0.28.11",
"extract-text-webpack-plugin": "=3.0.2",
"file-loader": "=1.1.5",
"image-webpack-loader": "=3.4.2",
"node-sass": "=4.7.2",
"postcss-loader": "=2.0.9",
"postcss-smart-import": "=0.7.6",
"sass-loader": "=6.0.6",
"style-loader": "=0.19.0",
"webpack": "=3.6.0"
Bundle
We want our compiled React apps to be as production-esque as we can make them - thus, we will include in our package.json
(as dependencies
and not development dependencies
):
"react": "=16.3.1",
"react-dom": "=16.3.1",
"react-redux": "=5.0.7",
"react-router-dom": "=4.2.2",
"redux": "=3.7.2"
The relevant part of our webpack.config.js
files is:
//...
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.jsx$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
//...
]
We'll also need to add the following .babelrc
to our a project root:
{
"presets": ["react", "es2015", "stage-0"],
"plugins": ["transform-decorators-legacy"]
}
I've included both, a development
and a production
version here to make life a little easier and because, hey, I <3 you.
$ npm run build-all
React Baseline
Our React baseline projects will include a Router
, a Redux
, and a CSS in JS
component since, at the minimum, most React apps will have as much:
import React from 'react'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import CssInJsComponent from './CssInJsComponent'
export default () =>
<BrowserRouter> <div> <nav> <ul> <li> <Link to={'/'}>Home</Link> </li> </ul> </nav> <Switch> <Route exact path={'/'} component={CssInJssComponent}/> </Switch> </div> </BrowserRouter>
import { dummy, DUMMY_ACTION } from './Actions'
export const DummyStorage = (state = {}, action) => {
switch (action['cmd']) {
case DUMMY_ACTION:
state[action['key']] = action['value']
return state
default:
return state
}
}
import React from 'react'
export default () => <div>Hallo Planetoid!</div>
The variable we'll be altering in each test case is our CssInJsComponent
. In certain cases (such as our Radium
and Styletron
test-cases), there will be some fairly extensive alterations to the baseline components specified above!
In each, we'll be testing the equivalent of:
div {
background-color: black,
color: white,
opacity: .9
}
Hardware
The increasing popularity of Dell's XPS laptops and the advent of Microsoft's Linux Subsystem on Windows 10 (which I just set up) gives developers a lot of flexibility when choosing hardware and budgeting.
This particular test suite was performed on a Dell 15 XPS from 2016 equipped with:
Intel Core i7-6700HQ Quad-Core (8 Logical Cores) at 2.60 GHz 32 GB RAM Windows 10 Pro + Linux Subsystem and Cygwin NVIDIA 960M GPU 256GB m2 PCIe SSD 15.6" FHD Screen
I'm also a big fan of Apple products (I use all the gadgets when I can) since they are required to use Swift and develop for iOS. The comparable version from Apple would be one of the new MacBook Pro 15's!
As of April 2018, the underlying hardware between the two makers is derived primarily from Intel, NVIDIA, or AMD (Radeon) so, despite some lessening differences between operating systems, laptop models are roughly the same under-the-hood. As a result, your findings should mirror the results of the article despite the particular model or make of the testing equipment.
Results
Let's go over the results!
Build Time and Bundle Size!
To run the tests yourself:
$ npm run build-all
To toggle between development
and production
modes alter:
module.exports = {
production: false
}
Unminified (Development):
Testing build time for development
-like scenarios (time to rebuild/recompile on change):
Library | Vendor Size | App Size | First Run | Second Run | Third Run | Average Run |
---|---|---|---|---|---|---|
Aphrodite | 938 KB | 4.02 KB | 4237 ms | 5450 ms | 4952 ms | 4879.66 ms |
CXS | 858 KB | 3.94 KB | 4952 ms | 5023 ms | 5399 ms | 5124.66 ms |
Emotion | 890 KB | 4.26 KB | 5093 ms | 6322 ms | 6015 ms | 5810 ms |
Glamorous | 1.02 MB | 3.96 KB | 5029 ms | 4911 ms | 6406 ms | 5448.66 ms |
JSS | 937 KB | 4.08 KB | 5816 ms | 5383 ms | 5967 ms | 5722 ms |
Radium | 1.02 MB | 6.04 KB | 7495 ms | 6416 ms | 6210 ms | 6707 ms |
Styled-Components | 990 KB | 4.29 KB | 6107 ms | 5732 ms | 5650 ms | 5829.66 ms |
Styletron | 875 KB | 4.07 KB | 6029 ms | 3597 ms | 5649 ms | 5091.66 ms |
Build Time Winner: Aphrodite (4879.66 avg ms)
Vendor Dependencies Size Winner: CXS (858 KB)
App Size Winner: CXS (3.94 KB)
Overall Size Winner: CXS (861.94 KB)
Minified (Production):
Testing build time and size for production
-like scenarios:
Library | Vendor Size | App Size | First Run | Second Run | Third Run | Average Run |
---|---|---|---|---|---|---|
Aphrodite | 184 KB | 1.76 KB | 10890 ms | 4952 ms | 9653 ms | 8498.33 ms |
CXS | 165 KB | 1.74 KB | 9806 ms | 5399 ms | 9895 ms | 8366.66 ms |
Emotion | 179 KB | 1.91 KB | 10251 ms | 6015 ms | 10628 ms | 8964.66 ms |
Glamorous | 230 KB | 1.72 KB | 13597 ms | 6406 ms | 12359 ms | 10787.33 ms |
JSS | 197 KB | 1.79 KB | 11285 ms | 5967 ms | 11029 ms | 9427 ms |
Radium | 230 KB | 2.77 KB | 11854 ms | 6210 ms | 12343 ms | 10135.66 ms |
Styled-Components | 205 KB | 1.91 KB | 11800 ms | 5650 ms | 11178 ms | 9542.66 ms |
Styletron | 179 KB | 1.79 KB | 10163 ms | 5649 ms | 10879 ms | 8897 ms |
Build Time Winner: CXS (8366.66 avg ms)
Vendor Dependencies Size Winner: CXS (165 KB)
App Size Winner: Glamorous (1.72 KB)
Overall Size Winner: CXS (166.74 KB)
live-server - Time To Complete 1000 Requests
In this test, we see how well the different frameworks hold-up against each other in high-use scenarios. We want to see how much throughput our server can handle given the choice of a particular CSS in JS framework (Please note that this is a test of server-side performance rather than client page-loading performance):
$ npm run number-request
Library | First Run | Second Run | Third Run | Average Run |
---|---|---|---|---|
Aphrodite | 3304 ms | 3302 ms | 3134 ms | 3246.66 ms |
CXS | 2811 ms | 2823 ms | 2994 ms | 2876 ms |
Emotion | 2606 ms | 2732 ms | 2756 ms | 2698 ms |
Glamorous | 2716 ms | 2524 ms | 2572 ms | 2604 ms |
JSS | 2681 ms | 2585 ms | 2657 ms | 2641 ms |
Radium | 2783 ms | 2573 ms | 2942 ms | 2766 ms |
Styled-Components | 2133 ms | 2551 ms | 2790 ms | 2491.33 ms |
Styletron | 2479 ms | 2457 ms | 2582 ms | 2506 ms |
The Winner: Styled-Components (2491.33 avg ms)
Phantom.js - Page Load Performance
In this test, we'll be testing the time to execute a request and receive a response back from our test server. We want to see how fast we can request, render, and receive a production
page when equipped with a particular CSS in JS framework:
$ npm run phantom-page
Library | First Run | Second Run | Third Run | Average Run |
---|---|---|---|---|
Aphrodite | 3121 ms | 3016 ms | 3230 ms | 3122.33 ms |
CXS | 2995 ms | 2983 ms | 3133 ms | 3037 ms |
Emotion | 3002 ms | 3042 ms | 2917 ms | 2987 ms |
Glamorous | 2947 ms | 3047 ms | 2968 ms | 2987.33 ms |
JSS | 2906 ms | 3018 ms | 3178 ms | 3034 ms |
Radium | 3000 ms | 2691 ms | 3118 ms | 2936.33 ms |
Styled-Components | 3012 ms | 2873 ms | 3075 ms | 2986.66 ms |
Styletron | 3031 ms | 3044 ms | 2995 ms | 3023.33 ms |
The Winner: Radium (2936.33 avg ms)
Phantom.js - I/O Performance
To further quantify overall I/O performance, we'll also test the time to load each supplied index.html
with all production
assets this time using Phantom.js and combining the results with our previous build size:
$ npm run phantom-io
Library | First Run | Second Run | Third Run | Average Run |
---|---|---|---|---|
Aphrodite | 57.3864 KB/S | 54.3953 KB/S | 56.3250 KB/S | 56.0355 KB/S |
CXS | 53.8044 KB/S | 50.7116 KB/S | 55.0842 KB/S | 53.2000 KB/S |
Emotion | 57.6513 KB/S | 57.6697 KB/S | 56.1657 KB/S | 57.1622 KB/S |
Glamorous | 75.1118 KB/S | 72.2544 KB/S | 85.9495 KB/S | 77.7719 KB/S |
JSS | 62.8684 KB/S | 65.2198 KB/S | 73.8447 KB/S | 67.3109 KB/S |
Radium | 77.0251 KB/S | 79.8251 KB/S | 71.2706 KB/S | 76.0402 KB/S |
Styled-Components | 66.6591 KB/S | 71.7441 KB/S | 62.3222 KB/S | 66.9084 KB/S |
Styletron | 59.7652 KB/S | 61.8508 KB/S | 62.3222 KB/S | 61.3127 KB/S |
The Winner: Glamorous (77.7719 avg KB/S)
Conclusion
Let's wrap this up!
Highlights
CXS is an overall strong contender taking almost all of the development build
first-place build spots and coming in third for the overall time at a respectable build time average of 5124.66 ms (versus Aphrodite's 4879.66 avg ms).
CXS also places first for almost every production build
spot. In systems where multiple builds are required daily, CXS is a great choice. CXS is also very fast in terms of clientside page loading.
From a server-side standpoint, Glamorous represents a strong choice - it comes first in our KB/S test (testing clientside load time against buildsize), third in our 1000 request test, and fourth in raw clientside page load time.
Radium reveals a similar profile coming in first for raw clientside page load performance and running middle of the pack for the other test cases.
Aphrodite underperformed and came in last or near last in all three non-build-related test scenarios.
Comparison to Previous Tests
HelloFresh is a great company both because it's tasty and for experimenting with CSS in JS tests previously. At that time, HelloFresh concluded there was no clear winner.
Like before, CXS remains a strong build and clientside page loading library capable of efficiently injecting styles at render.
This time, we've also tested for raw serverside performance and the metrics we've obtained for Glamorous and Radium mirrors their continued rise in popularity.
Check out the code and tests used in this article over on GitHub!