μλ¬Έ : An SSR Performance Showdown
π‘ λ ΈνΈ: 첫 λ²μ§Έ λ²μ μ λ²€μΉλ§ν¬μμ λͺ κ°μ§ μ€μκ° μμμΌλ©°, μ΄λ Theo Browneμ μ΄ νΈμμ μμ½λμ΄ μμ΅λλ€. λ¨Όμ μ΄λ¬ν μ€λ₯μ λν΄ μ¬κ³Όλ리며, Rich Harris, Ryan Carniato, Dan Abramov, BalΓ‘zs NΓ©meth, Dominic Gannawayμ μμ μ λν΄ μ§μ¬μΌλ‘ κ°μ¬μ λ§μμ μ ν©λλ€. λν Preact λ²μ μ κΈ°μ¬ν΄μ£Όμ Jovi De Croockμκ²λ κ°μ¬λ₯Ό νν©λλ€.
μλ² μ¬μ΄λ λ λλ§(SSR)μ Node.jsλ‘ κ³ μ±λ₯ μΉ μ ν리μΌμ΄μ μ ꡬμΆν λ μ’ μ’ κ°κ³Όλλ μΈ‘λ©΄μ λλ€.
컨μ€ν μ νλ©΄μ Node.js μ±λ₯ λ¬Έμ λ₯Ό λλ²κΉ νλ λ° μ€μ μ λλ κ²½μ°κ° λ§μμ΅λλ€. μ΄λ¬ν μν©μμ λ¬Έμ μ μμΈμ κ±°μ νμ SSRμ΄μμ΅λλ€. SSRμ CPUμ ν° λΆλ΄μ μ£Όλ μμ μΌλ‘, Node.jsμ μ΄λ²€νΈ 루νλ₯Ό μ°¨λ¨νλ μ£Όμ μμΈμ΄ λ μ μμ΅λλ€. λ°λΌμ νλ°νΈμλ μ€νμ μ νν λ μ΄ μ μ λ°λμ κ³ λ €ν΄μΌ ν©λλ€.
μ ν¬λ μ€λλ κ°μ₯ μΈκΈ° μλ λΌμ΄λΈλ¬λ¦¬μμ SSR μ±λ₯μ΄ μ΄λ€ μνμΈμ§ μμλ³΄κΈ°λ‘ νμ΅λλ€. νΉν Fastifyμ κΉλνκ² ν΅ν©ν μ μλ λΌμ΄λΈλ¬λ¦¬λ₯Ό μ§μ€μ μΌλ‘ μ΄ν΄λ³΄μμ΅λλ€.
μ΄λ₯Ό μν΄ λ§μ μμλ₯Ό ν¬ν¨ν 볡μ‘ν μν λ¬Έμλ₯Ό μμ±νμ¬ ν μ€νΈ νμ΄μ§λ₯Ό λ§€μ° ν¬κ² λ§λ€κ³ , κ° λΌμ΄λΈλ¬λ¦¬μ μ±λ₯μ μ ννκ² μΈ‘μ ν μ μλ μΆ©λΆν μ€ν μκ°μ ν보ν΄μΌ νμ΅λλ€.
κ·Έλμ LLMμκ² 10x10px νμΌλ‘ λ divλ₯Ό μ¬μ©νμ¬ μ»¨ν μ΄λμ λμ νμ 그리λ μ½λλ₯Ό μμ±ν΄ λ¬λΌκ³ μμ²νμ΅λλ€.
<script>
const wrapper = document.getElementById('wrapper')
const width = 960
const height = 720
const cellSize = 5
function drawSpiral() {
let centerX = width / 2
let centerY = height / 2
let angle = 0
let radius = 0
const step = cellSize
while (radius < Math.min(width, height) / 2) {
let x = centerX + Math.cos(angle) * radius
let y = centerY + Math.sin(angle) * radius
if (x >= 0 && x <= width - cellSize && y >= 0 && y <= height - cellSize)
{
const tile = document.createElement('div')
tile.className = 'tile'
tile.style.left = `${x}px`
tile.style.top = `${y}px`
wrapper.appendChild(tile)
}
angle += 0.2
radius += step * 0.015
}
}
drawSpiral()
</script>
μ΄ν, ν μ€νΈνλ €λ λͺ¨λ λΌμ΄λΈλ¬λ¦¬λ₯Ό μ¬μ©ν λ²μ μ μμ±νλλ‘ μμ²νμΌλ©°, μλ μμ μ DOM λ©μλμ μμ‘΄νμ§ μκ³ κ° λΌμ΄λΈλ¬λ¦¬μ λ λλ§ μμ§μ μ¬μ©νλλ‘ κ΅¬νμ μμ νμ΅λλ€.
λ€μμ 2398κ°μ <div>
μμλ₯Ό ν¬ν¨ν μν λ¬Έμμ λͺ¨μ΅μ
λλ€.
Fastifyμ Vite ν΅ν© μ€μ μ λ€μν νλ μμν¬μ SSR μ±λ₯μ μ‘°μ¬νκΈ° μν μλ²½ν ν μ€νΈ νκ²½μ μ 곡ν©λλ€.
μ΄ κΈμμλ SSRμ μννλ λ° νμν μ΅μνμ 보μΌλ¬ νλ μ΄νΈλ₯Ό μ΄ν΄λ³΄κ³ 5κ°μ§ μ£Όμ νλ°νΈμλ λΌμ΄λΈλ¬λ¦¬μ(React, Vue, Solid, Svelte, Preact) μ±λ₯μ λΉκ΅ν΄ 보μμ΅λλ€. λν λ κ°λ¨ν λμμΌλ‘ fastify-html(ghtmlμ Fastifyλ‘ λ©νν λ²μ ) λ° @fastify/viewλ₯Ό ν΅ν ejsλ μ΄ν΄λ΄€μ΅λλ€.
μ°λ¦¬λ Next.js, Astro, Qwik μ κ°μ λꡬλ€, κ·Έλ¦¬κ³ λ€λ₯Έ μμ ν νλ μμν¬λ€μ κ³ λ €νμ§ μκΈ°λ‘ νμ΅λλ€. κ·Έ μ΄μ λ μ΄λ€μ΄ λ 립μ μΈ λ λλ§ λ©μλλ₯Ό μ 곡νμ§ μκΈ° λλ¬Έμ λλ€.
@fastify/vite κΈ°λ° ν μ€νΈμ κ²½μ° λ€μκ³Ό κ°μ 보μΌλ¬ νλ μ΄νΈλ₯Ό μ¬μ©νμ΅λλ€.
import Fastify from 'fastify';
import FastifyVite from '@fastify/vite';
const server = Fastify();
await server.register(FastifyVite /* options */);
await server.vite.ready();
await server.listen({ port: 3000 });
λͺ¨λ ν
μ€νΈλ νλ‘λμ
λΉλ, μ¦ vite build
λ₯Ό μ€νν νμ μ€νλμμ΅λλ€.
μμΈκ° μλ€λ©΄ Viteκ° νμνμ§ μμ fastify-html λ° ejs ν μ€νΈμ λλ€.
λͺ¨λ μμ κ° ν¬ν¨λ μ μ₯μλ₯Ό νμΈνμΈμ.
μΌκ΄μ± 보μ₯
λͺ¨λ μμκ° λμΌν νΉμ±μ 곡μ νλλ‘ νμ΅λλ€.
-
ν΄λΌμ΄μΈνΈ μΈ‘ λ°μμ± κΈ°λ₯μ μ¬μ©νμ§ μμ΅λλ€.
-
React λ° Solidμ κ²½μ°μ²λΌ ν΄λΉ νλ μμν¬μ λΆμ μ ν κ²½μ°λ₯Ό μ μΈνκ³ λͺ¨λ μ€νμΌ λ°μΈλ©μ ν νλ¦Ώ 리ν°λ΄μ μ¬μ©νμ¬ μνλ©λλ€.
-
x
λ°y
κ°μtoFixed(2)
λ‘ λ§λ€μ΄μ§λλ€. -
<style>
νκ·Έλ document μ μλ§ μ‘΄μ¬ν©λλ€.
ν μ€νΈλ 2020λ ν λ§₯λΆ μμ΄ M1, 8GB RAM, λ§₯OS λ²€μΈλΌμμμ λ Έλ v22μμ μ€νλμμ΅λλ€.
fastify-html
μ΄μκ°(outlier)λΆν° μμνκ² μ΅λλ€. ghtmlλ₯Ό λννλ Fastify νλ¬κ·ΈμΈ fastify-htmlλ μ΄λΉ 1088κ°μ μμ²μ μ λ¬ν©λλ€. μμ μΈκΈνλ―μ΄ μ΄ μ€μ μ νΉλ³ν ꡬ문μ΄λ λ³νμ΄ νμνμ§ μμΌλ―λ‘ Viteκ° νμνμ§ μλ€λ μ μμ λ€λ₯Έ λͺ¨λ μ€μ κ³Ό λ€λ¦ λλ€.
fastify-htmlμ΄ μ΄ ν μ€νΈμ κΈ°μ€μ μΌλ‘ μΆκ°λμμ΅λλ€. μ΄ λΌμ΄λΈλ¬λ¦¬λ λ€λ₯Έ λΌμ΄λΈλ¬λ¦¬μ κ³ κΈ κΈ°λ₯μ΄ μλ λ¨μν HTML ν νλ¦Ώ λΌμ΄λΈλ¬λ¦¬μ λνΌμ λΆκ³ΌνκΈ° λλ¬Έμ λ€λ₯Έ λͺ¨λ λΌμ΄λΈλ¬λ¦¬μ μ λλ‘ λΉκ΅νκΈ°λ μ΄λ ΅μ΅λλ€. λ λ¨μνκΈ° λλ¬Έμ μ΄λ―Έ λ λμ μ±λ₯μ λ°νν κ²μΌλ‘ μμνκ³ , κ·Έμ λ€λ₯Έ λͺ¨λ κΈ°λ₯μ κ°μΆ λΌμ΄λΈλ¬λ¦¬λ€μ΄ μΌλ§λ λ€μ²μ§λμ§λ₯Ό μμλ³΄κ³ μΆμμ΅λλ€.
μ¬μ©λ 보μΌλ¬ νλ μ΄νΈλ μλμμ νμΈν μ μμ΅λλ€. document μ
Έμ λ λλ§νλ λ μ΄μμ ν¨μλ₯Ό λ±λ‘νλ λ° createHtmlFunction
(@fastify/vite λͺ¨λ°©)κ° μ¬μ©λλ κ²μ μ£Όλͺ©νμΈμ.
import Fastify from 'fastify';
import fastifyHtml from 'fastify-html';
import { createHtmlFunction } from './client/index.js';
const server = Fastify();
await server.register(fastifyHtml);
server.addLayout(createHtmlFunction(server));
μ°Έκ³ λ‘, ꡬμ EJS(@fastify/view κΈ°λ°)λ₯Ό μ¬μ©ν ν μ€νΈλ μΆκ°νμ΅λλ€. μ΄λΉ 443κ°μ μμ²μ μ²λ¦¬ν μ μμμ΅λλ€.
Vue
2μλ₯Ό μ°¨μ§ν Vueλ μ΄λΉ 1028건μ μμ²μ μ²λ¦¬νλ©°, λ°μ΄λ SSR μ±λ₯κ³Ό μμ ν λΌμ΄λΈλ¬λ¦¬ μνκ³λ₯Ό μνλ κ²½μ° μ΅κ³ μ μ νμΌ κ²μ λλ€.
λκΈ°μ μλ² μ¬μ΄λ λ λλ§μ μν΄ μ¬μ©λ Vue APIλ renderToString()
μ
λλ€.
import { renderToString } from 'vue/server-renderer';
// ...
await server.register(FastifyVite, {
async createRenderFunction({ createApp }) {
return async () => ({
element: await renderToString(createApp()),
});
},
});
Svelte
3μλ Svelte 5(μμ§ μΆμ μ )λ‘, μ΄λΉ 968건μ μμ²μ μ²λ¦¬νμ¬ λ§€μ° μΈμμ μΈ μ±λ₯μ 보μ¬μ£Όμμ΅λλ€. νΉν νλΆν κΈ°λ₯μ κ³ λ €νμ λ λμ± λλΌμ΄ κ²°κ³Όμ λλ€.
Svelteλ μ체μ μΈ non-JSX templating κ΅¬λ¬Έμ΄ μμΌλ©° μμ§μ΄ λ§€μ° ν¨μ¨μ μ΄μ΄μ μ±μν λΌμ΄λΈλ¬λ¦¬ μμ½μμ€ν μ κ°μΆ νλ μμν¬κ° νμνκ³ SSR μ±λ₯ μ νλ₯Ό μνμ§ μλ κ²½μ° νμν μ νμ΄ λ μ μμ΅λλ€.
μλ² μ¬μ΄λ λ λλ§μ μν΄ μ¬μ©λ Svelte APIλ Svelte 5μ render()
μ
λλ€.
await server.register(FastifyVite, {
root: import.meta.url,
createRenderFunction({ Page }) {
return () => {
const { body: element } = render(Page);
return { element };
};
},
});
render()
ν¨μλ head λ° bodyμ μμ±λ λ°νν©λλ€.
Solid
4μλ μ΄λΉ 907건μ μμ²μ μ²λ¦¬νλ SolidJSκ° μ°¨μ§νμ΅λλ€. Svelteλ³΄λ€ λ§€μ° κ·Όμν μ°¨μ΄λ‘ λ€μ²μ§λλ€. Solidλ Reactμ λ§€μ° μ λ§ν λμμ΄μ§λ§, μμ§ μνκ³κ° μ±μνμ§ μμ μνμ λλ€.
μ°λ¦¬κ° μ£Όλͺ©ν ν κ°μ§λ SolidJSκ° μ€μ λ‘ μν(Hydration) κ³Όμ μ μΌλΆλ‘ IDλ₯Ό μ¬μ©νλ λ° μ΄λ €μμ κ²ͺκ³ μλ€λ κ²μ λλ€. Vueμ Solid κ°κ°μμ μμ±λ λ§ν¬μ μ λΉκ΅ν΄ 보μΈμ.
<div class="tile" style="left: 196.42px; top: 581.77px">
<div data-hk=1c2397 class="tile" style="left: 196.42px; top: 581.77px">
μ΄λ μ±λ₯ μ νμ μλΉ λΆλΆμ΄ μ μ μΌλ‘ μ μ‘ν΄μΌ νλ μ΄ μΆκ° μ‘°κ°μμ λ°μνλ€λ κ²μ μλ―Έν©λλ€. νμ§λ§ μ ν¬λ μν λ±μ ν΄λΌμ΄μΈνΈ κΈ°λ₯μ νμ±νν μ μμ μΈ μ€μ μν©μμ νλ μμν¬κ° μ΄λ»κ² μλνλμ§ μ νν κ²μ¦νκ³ μΆμμ΅λλ€.
보μΌλ¬ νλ μ΄νΈμ κ²½μ°, @fastify/viteμ createRenderFunction
ν
μ μ¬μ©νμ¬ Solid μ»΄ν¬λνΈ ν¨μ(createApp
)λ₯Ό μΊ‘μ²νμ΅λλ€.
import { renderToString } from 'solid-js/web';
// ...
await server.register(FastifyVite, {
root: import.meta.url,
createRenderFunction({ createApp }) {
return () => {
return {
element: await renderToString(createApp),
};
};
},
});
Preact
Reactμ μΈκΈ° λμμΌλ‘ 5μλ₯Ό μ°¨μ§ν Preactλ μ΄λΉ 717건μ μμ²μ μ²λ¦¬ν©λλ€. Preactλ Reactμ λ§€μ° μ μ¬νμ§λ§, λ λΉ λ₯΄κ³ κ°λ³κ² λ§λλ λ§μ μ°¨μ΄μ μ΄ μμ΅λλ€.
λκΈ°μ μλ² μ¬μ΄λ λ λλ§μ μ¬μ©λλ Preact APIλ renderToString()
μ
λλ€.
import { renderToString } from 'preact-render-to-string';
// ...
await server.register(FastifyVite, {
root: import.meta.url,
createRenderFunction({ createApp }) {
return () => {
return {
element: renderToString(createApp()),
};
};
},
});
React
React 19 RCλ μ΄λΉ 572건μ μμ²μ μ²λ¦¬νλ©° 6μλ₯Ό μ°¨μ§νμ΅λλ€.
λκΈ°μ μλ² μ¬μ΄λ λ λλ§μ μ¬μ©λλ React APIλ renderToString()
μ
λλ€.
import { renderToString } from 'react-dom/server';
// ...
await server.register(FastifyVite, {
root: import.meta.url,
createRenderFunction({ createApp }) {
return () => {
return {
element: renderToString(createApp()),
};
};
},
});
λ§λ¬΄λ¦¬
π‘ κ·Έλ λ€λ©΄ μ΄ κ²°κ³Όλ 무μμ μλ―Έν κΉμ?
맨 μμλ fastify-htmlκ³Ό Vueκ° μκ³ κ·Έ λ€λ₯Ό Svelteμ Solidκ° λ°μ§ λ€μ«κ³ μμ΅λλ€. μλ§λ Vueμ Svelteκ° SSR μ±λ₯κ³Ό μνκ³ μ±μλ μ¬μ΄μμ κ°μ₯ μ’μ μ μΆ©μμ μ μν κ²μ λλ€.
μμ μΈκΈνλ―μ΄, 본격μ μΈ νλ°νΈμλ νλ μμν¬λ₯Ό μμ κ³ μ΅μνμ ν νλ¦Ώμ κ³ μν¨μΌλ‘μ¨ μ±λ₯μμ μ΄λ€ μ΄μ μ μ»μ μ μλμ§ λ³΄μ¬μ£ΌκΈ° μν΄ fastify-html ν μ€νΈκ° κΈ°μ€μ μΌλ‘ μΆκ°λμμ΅λλ€.
π νκ΅μ΄λ‘ λ νλ°νΈμλ μν°ν΄μ λΉ λ₯΄κ² λ°μλ³΄κ³ μΆλ€λ©΄ Korean FE Articleμ ꡬλ ν΄μ£ΌμΈμ!