Skip to content

Conversation

@gaearon
Copy link
Contributor

@gaearon gaearon commented Aug 28, 2024

Since the output is not dependent on any dynamic variables, it can be fully reused between requests.

This is a heuristic that production-grade React frameworks (e.g. Next.js) use by default.

Seems like this makes the result a few times faster.

Before (with #7)

│ Stat    │ 2.5%  │ 50%   │ 97.5% │ 99%   │ Avg      │ Stdev   │ Max   │
├─────────┼───────┼───────┼───────┼───────┼──────────┼─────────┼───────┤
│ Latency │ 10 ms │ 11 ms │ 29 ms │ 35 ms │ 13.29 ms │ 5.42 ms │ 55 ms │
└─────────┴───────┴───────┴───────┴───────┴──────────┴─────────┴───────┘
┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%  │ Avg    │ Stdev   │ Min    │
├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤
│ Req/Sec   │ 682    │ 682    │ 726    │ 734    │ 722.46 │ 15.08   │ 682    │
├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤
│ Bytes/Sec │ 134 MB │ 134 MB │ 143 MB │ 144 MB │ 142 MB │ 2.95 MB │ 134 MB │
└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘

After

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬───────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max   │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼───────┤
│ Latency │ 0 ms │ 0 ms │ 1 ms  │ 1 ms │ 0.15 ms │ 0.38 ms │ 10 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴───────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬───────────┬─────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg       │ Stdev   │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼───────────┼─────────┼─────────┤
│ Req/Sec   │ 13,687  │ 13,687  │ 14,871  │ 15,135  │ 14,778.55 │ 364.12  │ 13,686  │
├───────────┼─────────┼─────────┼─────────┼─────────┼───────────┼─────────┼─────────┤
│ Bytes/Sec │ 2.69 GB │ 2.69 GB │ 2.92 GB │ 2.97 GB │ 2.9 GB    │ 71.7 MB │ 2.69 GB │
└───────────┴─────────┴─────────┴─────────┴─────────┴───────────┴─────────┴─────────┘

:)

@mcollina
Copy link
Member

Unfortunately the point of this work is to analyze the impact of SSR. So it would make this essentially unfair.

@mcollina mcollina closed this Aug 28, 2024
@slorber
Copy link

slorber commented Aug 28, 2024

Not exactly the same but I was wondering if the React compiler isn't also suited to optimize React SSR?

For example in this base app, most of the JSX is static and can be shared between requests. Optimizing this manually is possible although not natural, but I think it would be fair to plug a compiler that optimizes this automatically?

CC @gsathya is the React Compiler only for client-side optimizations?

I tried wiring the compiler in Vite but the Vite React babel plugin doesn't seem to have any effect when using --ssr in Vite.

By that I mean something like this but more powerful:

https://babeljs.io/docs/babel-plugin-transform-react-constant-elements.html

The following "optimized app" would increase perf by ~10-15%, but it's not realistic for normal human beings to optimize 100% of their real production apps this way IMHO:

export function createApp () {
  return StaticAppElement
}
const StaticAppElement = (function createStaticJSX() {
  const wrapperWidth = 960
  const wrapperHeight = 720
  const cellSize = 10
  const centerX = wrapperWidth / 2
  const centerY = wrapperHeight / 2

  let idCounter = 0
  let angle = 0
  let radius = 0

  const tiles = []
  const step = cellSize

  let x
  let y
  while (radius < Math.min(wrapperWidth, wrapperHeight) / 2) {
    x = centerX + Math.cos(angle) * radius
    y = centerY + Math.sin(angle) * radius

    if (x >= 0 && x <= wrapperWidth - cellSize && y >= 0 && y <= wrapperHeight - cellSize) {
      tiles.push({ x, y, id: idCounter++ })
    }

    angle += 0.2
    radius += step * 0.015
  }

  return (
      <div id="wrapper">
        {tiles.map((tile) => (
            <div
                key={tile.id}
                className="tile"
                style={{
                  left: tile.x,
                  top: tile.y,
                }} />
        ))}
      </div>
  )
})()

Somehow, string template literal frameworks have this optimization "baked in" since strings are primitives/interned unlike objects: you don't need to recreate the string fragments on every SSR request, and only need to concat them.


Side note: I also tried to increase perf by using renderToStaticMarkup() instead of renderToString but the impact is very small: #13

@gaearon
Copy link
Contributor Author

gaearon commented Aug 28, 2024

I didn’t mean this PR too seriously, but I do want to point out that if Fastify positions itself as a minimal leaner alternative to frameworks, and as performance-conscious (to the point of doing benchmarks that recommend against using React), it would be good to point out where it’s behind state of the art performance optimizations that React frameworks can do. Pre-rendering static content at the build time is one of those optimizations, but I couldn’t find how to do that on the Fastify website. When looking at performance of different tools, I think it’s worthwhile to look at the big picture (how do rendering strategies affect performance, how do different frameworks let you shift work ahead of time, how they compare in terms of blocking on data vs streaming rendering while IO is loading) rather than only comparing the speed of emitting a thousand divs. This, IMO, would help readers develop a more nuanced and informed opinions.

@gaearon gaearon deleted the react-aot branch August 28, 2024 11:02
@mcollina
Copy link
Member

The point of the benchmark is not to recommend using React. In fact, we mostly use React at Platformatic.

Using a framework (or not) is often not dependent on the "rendering a thousand divs", but sometimes is. That's what this benchmark is for.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants