In web development, there are two main methods for rendering web pages: server-side rendering (SSR) and client-side rendering (CSR). Although both methods serve the same ultimate goal, they have different processes and advantages. Choosing the right rendering strategy when building web applications is crucial for optimizing performance, improving user experience, and enhancing SEO.
Edge Side Rendering (ESR) is a web rendering technique that involves pre-rendering web pages at the edge of the network, near the user, rather than at the origin server. The goal of ESR is to improve website performance by reducing the amount of data that needs to be transmitted over the network and by reducing the time it takes for the page to be rendered on the user's device.
In traditional web rendering, the user's browser sends a request to the origin server for a web page, and the server responds with the HTML, CSS, and JavaScript code for the page. The browser then parses and executes this code to render the page on the user's device.
In ESR, the edge server pre-renders the web page using the same rendering engine as the user's browser. The pre-rendered page is then stored on the edge server and served to the user's browser as a static image or video. This reduces the amount of data that needs to be transmitted over the network and speeds up the rendering process on the user's device.
ESR is often used in conjunction with content delivery networks (CDNs) to improve website performance for users located far from the origin server. By placing edge servers strategically around the world, CDN can deliver content to users faster and more efficiently than if the content was delivered directly from the origin server.
ESR significantly reduces server pressure and lowers the cost of maintaining the business compared to traditional Client Side Rendering (CSR) and Server Side Rendering (SSR) models.
In Client Side Rendering (CSR), the browser downloads the minimal HTML page and the JavaScript required for the page. JavaScript is used to update the DOM and render the page.
Advantages of CSR:
Disadvantages of CSR:
Server-side rendering (SSR) is a technology that renders web content on the server-side. The server generates the HTML, which is then sent to the client.
Advantages of SSR:
Disadvantages of SSR:
CSR and SSR each have their own advantages. To integrate the benefits of both rendering modes, many technical attempts have been made in the industry (SSG, ISR, isomorphic rendering, etc.). Edge Side Rendering(ESR) solution introduced in this article breaks out of the limitations of optimizing rendering from a technical perspective and proposes a completely new architecture for solving page rendering problems.
EdgeOne Edge Function provides a Serverless code execution environment on edge nodes. By writing business function code and setting trigger rules, you can run code elastically and securely on edge nodes close to users without configuring and managing server infrastructure.
With the Serverless environment provided by EdgeOne Edge Function, we can move the code that originally runs on the server to edge nodes closer to users. This solves the problems of long SSR back-end request time and heavy server pressure while retaining the advantages of fast first-screen loading speed and good SEO support.
At the same time, because ESR still maintains the client-server mode (the server of ESR is the edge node), optimization solutions such as isomorphic rendering can also be perfectly transplanted to the ESR scheme.
With the global edge node network and EdgeOne's Edge Function Serverless environment, ESR performs page rendering on edge nodes close to users, which has the following advantages:
After understanding the advantages of ESR, we will introduce how to develop and deploy code to achieve the ESR edge rendering solution.
Edge functions run on the Edge Runtime. Edge Runtime is built on the V8 JavaScript engine and its API design follows Web Standards. Therefore, we can use a variety of technology stacks for ESR edge rendering in edge functions, including but not limited to:
Generating HTML content directly and responding to the client:
const html = `...HTML...`;
async function handleEvent(event) {
const res = new Response(html, {
headers: { 'Content-Type': 'text/html' },
});
event.respondWith(res);
}
addEventListener('fetch', handleEvent);
Using a template engine to build HTML and responding to the client, taking art-template as an example:
import template from 'art-template/lib/template-web';
const tpl = `
...
<title><%= data.title %></title>
...
`;
function handleEvent(event) {
const html = template.render(tpl, {
data: {
title: 'ESR - ART-TEMPLATE - EdgeFunctions',
...
},
});
...
}
...
SSI (Server Side Includes) is a technology that embeds dynamic content into static HTML pages on the server-side. It uses special comment tags to indicate the content that needs to be inserted. Some legacy projects may use Nginx SSI to assemble HTML files, and we can also use edge functions to implement basic SSI.
const body = `...BODY...`;
const footer = `...FOOTER...`;
async function handleSSI(html) {
const ssiRegex = /<!--#include virtual="(.+?)" -->/g;
const replacements = {
'/body.html': body,
'/footer.html': footer,
};
return html.replace(ssiRegex, (_, includePath) => {
return replacements[includePath] || '';
});
}
async function handleEvent(event) {
const html = `
<!DOCTYPE html>
<html>
...
<!--#include virtual="/body.html" -->
<!--#include virtual="/footer.html" -->
</html>
`;
const processedHtml = await handleSSI(html);
...
}
...
Most web developers prefer to use frameworks like React or Vue.js to build interfaces, and we can also implement them in edge functions.
Here is an example of using React to render a simple component:
import React from 'react';
import { renderToString } from 'react-dom/server.browser';
function Home() {
...HOME...
}
async function handleEvent(event) {
const content = renderToString(<Home />);
const html = `
<html>
...
<body style="padding: 40px">
<div>${content}</div>
</body>
</html>
`;
...
}
...
Now let's talk about how to handle routing in edge rendering scenarios. In the scenarios mentioned earlier, such as HTML direct output, template engine, and Nginx SSI, these modes are suitable for simple page generation requirements, so routing can be manually handled or using the ef-manaia framework.
Below we mainly introduce the React + React Router solution. In edge functions, we can use react-router-dom to handle routing.
import React from 'react';
import { renderToString } from 'react-dom/server.browser';
import { Route, Routes, Link } from 'react-router-dom';
import { StaticRouter } from 'react-router-dom/server';
function Home() { ...HOME... }
function Blog() { ...BLOG... }
function App() {
return (
...
<Routes>
<Route path="/" element={<Home />} />
<Route path="/blog" element={<Blog />} />
</Routes>
...
);
}
async function handleEvent(event) {
const url = new URL(event.request.url);
const path = url.pathname;
const html = renderToString(
<StaticRouter location={path}>
<App />
</StaticRouter>,
);
...
}
...
Rendering different components based on different routes:
Traditional SSR usually waits until the server fully renders the entire page before sending it to the client, resulting in a longer response time.
Streaming rendering improves this by using the Chunked Transfer Encoding feature of HTTP 1.1 and the progressive parsing and rendering features of browsers to gradually send HTML chunks to the client, allowing the browser to gradually display the page.
This technology fully utilizes the advantages of SSR and streaming transmission, providing a smoother user experience and faster performance. We can also use edge functions to implement streaming rendering.
In edge functions, we can use the TransformStream API to create a readable and writable stream to achieve streaming writing of data:
async function sleep() { ... }
async function streamHTMLContent(writable) {
const writer = writable.getWriter();
await writer.write(`
...
<div>First segment</div>
`);
console.log(await sleep());
await writer.write(`
<div>Second segment</div>
...
`);
await writer.close();
}
async function handleEvent(event) {
const { readable, writable } = new TransformStream();
streamHTMLContent(writable);
const res = new Response(readable, {
headers: { 'Content-Type': 'text/html' },
});
event.respondWith(res);
}
...
In edge functions, we can use React's Suspense and React-DOM's renderToReadableStream API to achieve streaming rendering in the React ecosystem.
Modify the edge function entry file code:
import React from 'react';
import { renderToReadableStream } from 'react-dom/server.browser';
...
import Blog from './components/Blog';
import Home from './components/Home';
function App() { ...APP... }
async function handleEvent(event) {
...
const stream = await renderToReadableStream(
<StaticRouter location={path}>
<App />
</StaticRouter>,
);
const res = new Response(stream, {
headers: { 'Content-Type': 'text/html' },
});
event.respondWith(res);
}
...
Split the Blog component into a separate file and modify it as follows:
import React, { Suspense, lazy } from 'react';
...
const LazyContent = lazy(() => import('./Content'));
function Blog() {
return (
...
<Suspense fallback={<div>Loading...</div>}>
<LazyContent />
</Suspense>
...
);
}
export default Blog;
Create Content.jsx component:
import React from 'react';
async function sleep() { ... }
let data;
const getData = () => {
if (!data) {
data = sleep();
throw data;
}
if (data?.then) {
throw data;
}
const result = data;
data = undefined;
return result;
};
function Content() {
const data = getData();
...
}
export default Content;
From the above image, we can see that for the blog request, we can see the content at 207ms, and the asynchronous blog content fragment is streamed in the response at around 1s. The entire Content Download action lasted for 1.15s.
Isomorphic rendering refers to using the same code on the server-side and client-side to render the application. This approach combines the advantages of server-side rendering (SSR) and client-side rendering (CSR).
In isomorphic rendering, when a page is first accessed, the server generates the complete HTML page. This can make it faster to show the page content to the user, improve the first screen loading speed, and also be beneficial for search engine optimization (SEO). Once the page is sent to the browser, JavaScript takes over and turns it into a single-page application (SPA), and all subsequent interactions are completed on the client-side.
Modern JavaScript frameworks such as React and Vue.js support isomorphic rendering. In React, we can use the renderToString API to render components on the server-side, and then use the ReactDOM.hydrateRoot method on the client-side to hydrate the server-rendered static HTML into an interactive application. In edge functions, we can also use the hydrate feature to implement isomorphic rendering.
First, we need to split out the App component:
import React from 'react';
...
function App() {
return (
<html>
...
<script src="/client/index.js"></script>
</html>
);
}
export default App;
At the same time, we can modify the Blog code to use hooks:
import React, { useEffect, useState } from 'react';
...
async function sleep() { ... }
function Blog() {
const [data, setData] = useState('Loading');
const asyncData = async () => {
await sleep();
setData('Blog Content');
};
useEffect(() => {
asyncData();
}, []);
return (
...
<div>{data}</div>
...
);
}
export default Blog;
In isomorphic rendering mode, we need to bundle both server-side and client-side code. The server-side code uses renderToString to output HTML, and the client-side code uses hydrateRoot for hydration.
// server.jsx
import React from 'react';
import { renderToString } from 'react-dom/server.browser';
import { StaticRouter } from 'react-router-dom/server';
import App from './App';
async function handleEvent(event) {
...
const html = renderToString(
<StaticRouter location={path}>
<App />
</StaticRouter>,
);
...
}
...
// client.jsx
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
hydrateRoot(
document,
<BrowserRouter>
<App />
</BrowserRouter>,
);
In isomorphic rendering mode, the server is only responsible for rendering the first screen:
In server-side rendering mode, the server renders the first screen and loads index.js. Subsequent asynchronous requests (such as sleep) are initiated on the client-side after index.js has finished loading:
Tencent EdgeOne Edge Function has advantages such as distributed deployment, proximity to users, ultra-low latency, elastic scaling, and Serverless environment. With the help of Tencent EdgeOne Edge Function, we have proposed and implemented the ESR edge rendering solution.
The ESR edge rendering solution has advantages such as lower latency, faster page loading speed, reduced server burden, better caching strategy, and better scalability. This makes the ESR edge rendering solution an effective method to improve web application performance and user experience. We have now launched a free trial, click here or contact us for more information.