EdgeOne Logo
Tencent EdgeOne Blog
Tutorial

How to Implement Edge Side Rendering with Tencent EdgeOne Edge Function?

Tencent EdgeOne - Product Manager

Mask groupesr.png

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.

What is ESR?

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.

a865e235-0d84-4ac7-b20d-4814e3d4e1f3.png

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.

Traditional Rendering Method

What is CSR?

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:

  1. Better user experience: CSR allows for a more dynamic and interactive user experience, as the page can be updated in real-time based on user interactions.
  2. Lower server load: Since the rendering is done on the client side, the server only needs to send the initial HTML and JavaScript files, which can reduce server load and improve scalability.
  3. Easier development: With CSR, developers can use a wider range of front-end technologies and frameworks to build the user interface, making it easier to develop complex and responsive websites.

Disadvantages of CSR:

  1. Initial load time: The initial load time of a CSR-based website can be slower, as the browser needs to download and execute the JavaScript code before the page can be rendered.
  2. Slow performance on slow devices: If the user's device is slow or has limited resources, the rendering of a CSR-based website can be slow and may affect the user experience.
  3. SEO issues: Since the content of a CSR-based website is not initially visible to search engines, it may be more difficult to optimize for search engines.

What is SSR?

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:

  1. Faster initial load time: Server-side rendering (SSR) allows the server to generate the complete HTML page before sending it to the client, resulting in a faster initial load time.
  2. Better SEO: Since the complete HTML is rendered on the server, search engines can easily index the content, which can improve the website's search engine ranking.
  3. Simpler development: With SSR, the server is responsible for generating the HTML, which can simplify the development process for some applications.

Disadvantages of SSR:

  1. Higher server load: SSR requires the server to generate the complete HTML page for each request, which can increase the server load and make it harder to scale.
  2. Less dynamic user experience: Since the page is rendered on the server, it can be less dynamic and interactive, as the server cannot update the page in real-time based on user interactions.
  3. Limited front-end technologies: With SSR, the server is responsible for generating the HTML, which can limit the use of front-end technologies and frameworks to build the user interface.

Edge Side Rendering

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:

  • Fast page loading speed: distributed edge nodes are close to users, avoiding delays caused by back-end requests;
  • Good user experience: page rendering is completed on the edge node, avoiding white screens caused by loading JS;
  • Low service pressure: distributed edge nodes have load balancing, which can easily cope with extreme situations such as request spikes.

How to Implement Edge Side Rendering?

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:

1. HTML Direct Output

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

ef.png

2. Art-template

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',
      ...
    },
  });
  ...
}
...

at.png

3. Nginx SSI

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

ssi.png

4. React

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>
`;
  ...
}
...

React.png

5. React Router

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:

rr.png

blog.png

6. Streaming Rendering

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.

Native 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);
}
...

 React Streaming Rendering

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;

nw.png

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.

6. Isomorphic Rendering

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:

ir.png

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:

sleep.png

Conclusion

d90b8f67ff843ee7ecef4d80cc7393dc.png

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. You are welcome to Contact Us for more information.

Edge Function
Intro
Develop
Web