What this article is about?
Creating simple static server components at working project with loadable components (without next.js)
Current setup
The site works on a standard setup: SSR render static HTML with critical CSS at <head>
, and has React hydration on the client to make it interactive.
Site's HTML is generated with a JSON tree, with widgets structure, coming from DB+CMS. It is a fastify app on server, with isomorphic factory. It gets JSON and includes selected components on the page. Every component has a manifest file with loadable JSON - it describe widgets names with code endpoints and import names.
Main idea
Write a simple client widget to directly render HTML from the server, and don't load widget chunk to client.
Let's start
Get one of the largest static widgets - markdown. It weight - 43kb (min+gzip)
Change the manifest by splitting the common link into the server and client.
Current setup:
MarkdownContainer: {
component: loadable(
() =>
import(/* webpackChunkName: "MarkdownContainer" */ "./desktop"),
{
resolveComponent: (mod) => mod.MarkdownContainer,
}
),
},
Change to
MarkdownContainer: {
componentServer: loadable(
() => import(/* webpackChunkName: "MarkdownContainer" */ "./desktop"),
{
resolveComponent: (mod) => mod.MarkdownContainer,
}
),
componentClient: loadable(
() =>
import(
/* webpackChunkName: "MarkdownContainer" */ "~/components/fake-component"
),
{
resolveComponent: (mod) => mod.FakeTransformer,
}
),
SSRC: true,
},
MarkdownContainer_css: {
component: loadable(
() =>
import(/* webpackChunkName: "_css_MarkdownContainer" */ "./desktop"),
{
resolveComponent: (mod) => mod.MarkdownContainer,
}
),
},
Now let's look to componentServer
and componentClient
endpoints.
(About new widget's name MarkdownContainer_css
read in the middle of this article)
We allready have 2 webpack configurations: one for the client and one for the server, resulting in 2 builds. These endpoints we transform at webpack loaders: remove componentClient
from server loader, and remove componentServer
from client loader. (use jscodeshift
for example)
Server and client part of widget is connected via identical chunk name MarkdownContainer
.
SSRC: true
- it is a flag for widgets factory. At this point we must remember, that it is an isomorphic code, and first time it is working at server, and second time - at client.
At the end of standard recursive factory func add:
// server
if (SSRC) {
reactElement = createElement(ServerTransformer, { SSRC }, reactElement);
}
// client
if (properties?.__html) {
reactElement = createElement(ClientTransformer, { __html: properties.__html });
}
At server render, if widget's data has SSRC
key, we create wrapper with prop SSRC
.
At client render, if we have html-rendered string, we create client wrapper.
Here ServerTransformer
and ClientTransformer
code:
const ServerTransformer = ({ children }) => (
<section>{children}</section>
);
const ClientTransformer = ({ __html }) => (
<section dangerouslySetInnerHTML={{ __html }} />
);
Component FakeTransformer
just return null.
You see, that all transformers wrap codewith section
tag. You can wrap by <>
at server and <section>
on client and change html by useEffect, but you will catch hydration mismatch.
Now, our build is ready to use. When a user requests the page, we need to perform some tasks on the server side and modify the server router.
When we transform JSON into a React tree, we need to perform some changes.
const Renderer = ({...}) => (
<Consumer>
{({ extractor, widgets }) => {
const res = createReactTree(widgets, device, ...);
if (extractor) {
checkSSRC(res, widgets, extractor);
}
return res;
}}
</Consumer>
);
Check tree and modify json state (it will sent to client at html body)
const checkSSRC = (tree, widgets, extractor) =>
(tree.length ? tree : [tree]).map((elem, tree_i) => {
if (elem?.props?.children) {
// Recursive
checkSSRC(elem.props.children, widgets[tree_i].children, extractor);
}
if (elem?.props?.SSRC) {
const extractorClone = Object.assign(
Object.create(Object.getPrototypeOf(extractor)),
extractor
);
let __html = renderToString(extractorClone.collectChunks(elem));
// don't need any props now
widgets[tree_i].properties = { __html };
// don't need any children now
widgets[tree_i].children = [];
}
return elem;
});
As you see, we have an instance of css generator from current react node, to have a css classes. At the top of code sample, you have seen a new widget with _css
postfix - it needs to get css-classes and styles for critical css. Without it, we only have classes at html, and no styles.
It's all done! Now we can check old/new HTML of the page, and see result.
HTML structure:
- Critical CSS
- HTML content
- React state
- Resources
1. Critical CSS
The same or less. If component has multi-designs, will be selected only needed classes/styles;
2. HTML content
The same, but server components wrapped with <section>
tag. If you not need, you can add some code to ClientTransformer
component (and have hydration mismatch?)
useEffect(() => {
if (!ref.current) return;
// make a js fragment element
const fragment = window.document.createDocumentFragment();
// move every child from our div to new fragment
while (ref.current?.childNodes[0]) {
fragment.appendChild(ref.current?.childNodes[0]);
}
// and after all replace the div with fragment
ref?.current?.replaceWith(fragment);
}, [ref]);
3. React state
Old
{
"name": "Markdown",
"properties": { "content": "Have **bold** text" }
}
New
{
"name": "Markdown",
"properties": {
"__html": "\u003Csection\u003E\u003Cp class=\"a1jIK lG2mw _2G2mw a2WsA b2WsA\"\u003EHave \u003Cspan class=\"a1jIK w1jIK\"\u003Ebold\u003C\u002Fspan\u003E text\u003C\u002Fp\u003E\u003C\u002Fsection\u003E"
}
}
(__html
: This is a HTML-string, converted to JSON)
<section>
<p class="a1jIK lG2mw _2G2mw a2WsA b2WsA">
Have <span class="a1jIK w1jIK">bold</span> text
</p>
</section>
4. Resources
Less JS/CSS chunks.
Downloaded JS decreases from 656Kb
to 570Kb
(-86Kb, localhost, not minified data. In my case, was changed Text, Button and MarkDown widgets. ).
This article not a "Hello word" type, but I hope this help you to undestand some of server components, promote to know more about it, fix bugs and give me advises to optimize code...
and make the web fast again!
P.S. Look for my previous post Reduce bundle size via one-letter css classname hash strategy
The text was edited with AI to make the english stronger.
Donation: Ethereum 0x84914da79c22f4aC6fb9D577C73E29E4AaAE7622
Top comments (1)
Great article! Thank you!