This document outlines the results of an attempt to get Preact working with the Electrode platform. Getting this to work required two minor adjustments to the client archetype and server:
To get Preact working with the client bundle all I had to do was alias react
and react-dom
to preact-compat
alias: {
"react": "preact-compat",
"react-dom": "preact-compat"
}
Since we don't bundle server code this was a little more difficult (and hacky), but I got it working by monkey-patching require
and redirecting and require
requests to react
and react-dom
to preact-compat
const Module = require("module");
const _require = Module.prototype.require;
Module.prototype.require = function() {
let [pkg, ...args] = arguments;
if (pkg === "react" || pkg === "react-dom") {
pkg = "preact-compat";
}
return _require.apply(this, [pkg, ...args]);
}
The results below compare bundle size, TTFB, First meaningful paint, time to interactice, input latency, and a few others.
The bundle size was reduced from 254.23 kB
to 137.93 kB
, a reduction of 116.3 kb
which is substantial.
I measured TTFB in three scenarios:
- React with React.renderToString
- This is what we're doing right now, and represents the baseline
- preact-compat with React.renderToString
- Client code is built with
preact-compat
but still rendered on the server withReact.renderToString
- Client code is built with
- preact-compat on client and server
- Client and server code uses
preact-compat
, meaning that the content is being rendered on the server withpreact-render-to-string
- Client and server code uses
Metric | min | max | average | median | stddev |
---|---|---|---|---|---|
bodySize [bytes] | 660190 | 848878 | 667478.18 | 661003 | 26209.09 |
contentLength [bytes] | 848878 | 848878 | 848878 | 848878 | 0 |
httpTrafficCompleted [ms] | 432 | 589 | 510.16 | 506 | 24.74 |
timeToFirstByte [ms] | 138 | 215 | 168.18 | 166 | 18.27 |
timeToLastByte [ms] | 161 | 241 | 191.42 | 189 | 18.82 |
The median value for the TTFB is 166ms
. Note that we are using median over average,
since it more accurately reflects the average value when there are potential extreme outliers.
Metric | min | max | average | median | stddev |
---|---|---|---|---|---|
bodySize [bytes] | 282976 | 732572 | 566204.44 | 551875 | 79152.84 |
contentLength [bytes] | 732572 | 732572 | 732572 | 732572 | 0 |
httpTrafficCompleted [ms] | 462 | 1577 | 630.6 | 510.5 | 240.41 |
timeToFirstByte [ms] | 140 | 552 | 205.5 | 169.5 | 93.69 |
timeToLastByte [ms] | 162 | 728 | 239.36 | 194 | 118.44 |
This build uses preact-compat
for the client build, but does not change any over the SSR code. The median sits at 169.5
,
which is nearly identical to the results we got without preact-compat
. This makes sense, as our server code is still rendering regular React components using React.renderToString
, so this metric should not change.
This uses the same build used previously, but forces the server to use preact-compat
as well. This means internally the application is being rendered on the server with preact-render-to-string
.
Metric | min | max | average | median | stddev |
---|---|---|---|---|---|
bodySize [bytes] | 544288 | 731584 | 562356 | 552057 | 46873.42 |
contentLength [bytes] | 731584 | 731584 | 731584 | 731584 | 0 |
httpTrafficCompleted [ms] | 288 | 949 | 383.26 | 372 | 87.21 |
timeToFirstByte [ms] | 38 | 95 | 54.32 | 49.5 | 13.39 |
timeToLastByte [ms] | 61 | 122 | 77.64 | 72 | 14.13 |
The median TTFB drops down to 49.5
, which is 120ms
faster than React.renderToString
. This seems like a pretty clear indiciation that preact-render-to-string
can out perform React.renderToString
even without any caching optimizations.
The next set of metrics were obtained using Lighthouse and represent the average of 5 reports each.
The averge first meaningful paint with React occured after 4932.5ms
. The average first meaningful paint
with Preact using preact-compat
was 4307.3ms
, which is 625.5ms
faster.
Time to Interactive (TTI) measures how long until the app appears ready to interact with. This metric is considered alpha right now and should be considered with that in mind.
The average TTI for React was 5197.9ms
. The average TTI for Preact using preact-compat
was 4397.3ms
, which is 800.2ms
faster.
Input latency measures how long until the app will respond to user input.
The average estimated input latency for React was 62.54ms
. The average estimated input latency for Preact using preact-compat
was 66.64ms
, which was 3.86ms
slower.
Preact offers a reduced bundle size, faster server render times, and better client rendering performance over React even when using a compatability layer (preact-compat
) that offers robust support for existing React codebases. The SSR package preact-render-to-string
has APIs that would let us optimize performance further, and the maintainer Jason Miller has expressed interest in supporting caching and other optimizations natively.
This is speculation, but I would expect see further performance benefits once preact-compat
is removed and the codebase uses Preact's API directly. It appears this process could be handled gradually as well, which is a plus.
would be nice to get a measurement of actual time
React.renderToString
compare toPReact.renderToString
on the same component, and do some comparison on their HTML output.