Skip to content

Instantly share code, notes, and snippets.

@aweary
Last active June 4, 2019 14:20
Show Gist options
  • Save aweary/8781b9f509caed67cb432c8a2e5fcad4 to your computer and use it in GitHub Desktop.
Save aweary/8781b9f509caed67cb432c8a2e5fcad4 to your computer and use it in GitHub Desktop.

Preact for Electrode

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:

Client

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"
  }

Server

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]);
}

Results

The results below compare bundle size, TTFB, First meaningful paint, time to interactice, input latency, and a few others.

Bundle Size

The bundle size was reduced from 254.23 kB to 137.93 kB, a reduction of 116.3 kb which is substantial.

Time to First Byte (TTFB)

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 with React.renderToString
  • preact-compat on client and server
    • Client and server code uses preact-compat, meaning that the content is being rendered on the server with preact-render-to-string

React with React.renderToString

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.

preact-compat on client with React.renderToString

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.

preact-compat on client and server

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.

First Meaningful Paint

React

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 (experimental)

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.

Estimated Input Latency

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.

Summary

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.

@jchip
Copy link

jchip commented Dec 13, 2016

would be nice to get a measurement of actual time React.renderToString compare to PReact.renderToString on the same component, and do some comparison on their HTML output.

@aweary
Copy link
Author

aweary commented Dec 13, 2016

@jchip TTFB indirectly measures that, since essentially all else is held equal between the two benchmarks. I'll add those numbers as well though.

@jchip
Copy link

jchip commented Dec 13, 2016

so preact is compatible with https://github.com/ReactTraining/react-router?

@jchip
Copy link

jchip commented Dec 13, 2016

yeah, TTFB measures that. A direct tight loop that measures just renderToString would be more precise though.

@developit
Copy link

@jchip - yes, it's compatible with react-router.
TTFB might actually be a better metric than timing renderToString(), because it better takes into account the effect of memory consumption on delivering an HTTP response. One of the reasons preact-render-to-string (and preact itself for that matter) is faster is because it creates fewer objects, resulting in smaller and less frequent garbage collection sweeps. Depending on the setup, timing a set number of renderToString() calls may not even highlight that issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment