What is the React Server Component?

On December 21, 2020, React officially announced a new feature that is still in progress: the React Server Component. A brief description of the React Server Component appears on the React Server Component blog:

Introducing Zero-Bundle-Size React Server Components

The concept of zero-bundle-size cannot be understood in just a few words. What are React Server Components?

After some research, I’ve figured out what the React Server Component is:

  • First, the React Server Component is also a React Component. The word “server” was added because the React Server Component’s code execution environment is on the server. To distinguish component code from what we write everyday, the React Component code is executed in the browser environment. We call the former “React Server Component” and the latter “React Client Component”.

  • Second, we can distinguish between “React Server Component” and “React Client Component” by the way the component files are named. Js (/ts/ JSX/TSX). The React Client Component must end with.client.js(/ts/ JSX/TSX). While this naming scheme is tentative, it reflects a fundamental fact: we need to distinguish the React Server Component from the React Client Component at the file naming level. This is done because Bundlers, Linter, and other related tools need to distinguish between the two so they can adopt different operational strategies. So, we can use the file name to understand what React Server Components are.

  • Finally, “React Server Component” is a React component in the narrow sense, but a new rendering technology in the React community in the broad sense. This rendering technology is between pure Client rendering and pure Server rendering, and is not the same as SSR (Server side rendering). We can call this “mixed rendering.” At the same time, this feature, once released, will mark the server’s formal inclusion into the category of front-end programming environment.

Background of generation

Since the big companies led by Google vigorously promoted Ajax technology, the front-end has been deeply rooted in the field of “thick client” with the help of browsers. Now, we’re fine. Why would we bother? To be honest, the decision to “find trouble” is not in the hands of us civilians, but in the hands of the tycoons, or more accurately, the React core team. The React Server Component was developed by Dan and RFC: React Server Components. Front-end development faces three trade-offs: 1) user experience; 2) Maintenance costs; 3) Performance. When we design implementations, we often struggle to prioritize user experience, maintenance costs, and performance. The React core team felt we could have it all and truly have it both ways. This is the context proposed by the React Server Component. Simply put, the front end wants more.

1. We want a smaller packing volume

For the sake of performance, in order to load the first screen faster, the front end in reducing the volume of the first screen loading JS code is really rack their brains. On-demand import, tree-shaking, lazy load, and code split are all in the mix. These solutions do reduce the size of the first screen loading JS code somewhat. However, React officials feel that these solutions don’t do much to address the run-time code overload introduced by using large third-party libraries (for example, using a markdown library to parse markdowns or using a date library for formatting) in a common scenario.

In the course of daily development, we can’t write all the code, we tend to use third party libraries. This is a problem if the library is large and not tree-shaking. For a computation task, we need to transfer this bulky JS class library to the browser, parse and execute it to create the run time needed for the computation task. From this point of view, we are using a lot of bandwidth and CPU resources for a small computing task. To render a note written in Markdown, we need 240KB of JS code (74KB after gzip) as the runtime:

// NoteWithMarkdown.js
// NOTE: *before* Server Components

import marked from 'marked'; / / 35.9 K (gzipped 11.2 K)
import sanitizeHtml from 'sanitize-html'; / / 206 K (gzipped 63.3 K)

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}
Copy the code

But there is a fact in front of us. In the first screen we load, notes are just for viewing, and at this point it’s purely static (no user interaction is required). So if we can render it static on the server, don’t we save the cost of transferring a lot of JS code to the client, parsing and executing it? With the React Server Component, we can do just that. Of course, you could say that traditional pure server rendering or SSR could do a little bit of that. That’s true, except React Server Componen doesn’t just do that, it does something else, too, which comes later. It’s also worth noting that the React Server Component, when executed on the server side, does not return an HTML string, but a Custom React JSON-like string.

By changing the React Server component from the example above to the React Server Component, we can avoid transferring 240KB js code to the client when the first screen loads:

// NoteWithMarkdown.server.js - Server Component === zero bundle size

import marked from 'marked'; // zero bundle size
import sanitizeHtml from 'sanitize-html'; // zero bundle size

function NoteWithMarkdown({text}) {
  // same as before
}
Copy the code

Instead, 240KB of javascript code is executed on the server, which eventually returns the result (a custom JSON-like string) to the client. However, compared to 240KB, the size of these JSON-like strings is negligible or not considered packaged code. That’s exactly what officials say zero-bundle-size means.

2. We want server power

The front-end has been hard at work on the client side for more than a decade, ever since Ajax opened the “fat client”, and the ability to exploit it seems to have reached a bottleneck. In recent years, as the NodeJS ecosystem matures and applications become more widespread, the front-end industry is again focusing on the server – we are trying to get more capabilities from the server to enhance the front-end capabilities. So it seems, the ancients honestly do not deceive me. The trend of the world will be divided after a long period of unity, and will be united after a long period of separation. The same is true of front-end and back-end differentiation and convergence. From the division of work without a Frontend, to the epidemic of fat clients, to the current BFF (Backend For Frontend),SSR and React server component’s front and Backend refusion, the history is surprisingly similar.

No matter BFF, SSR or the Server Component proposed by the React core team, they all want to enhance the front-end capability with the help of the server capability. The difference this time around is that the React team, in an official tone, is clear and focused on the data manipulation capabilities that server programming provides. For example, they point out in the RFC that by intervening in the server, the front-end can gain the following [data manipulation capability] :

  • The ability to freely integrate back-end apis. This is also the original intention of the BFF middle layer. By freely integrating the back-end API, we can solve two problems in data acquisition in the current “fat client” : “excessive network round-tripping” and “data redundancy”.
  • Freedom to determine how data is stored on the server. For example, we are free to decide whether data is stored in memory, in files, or in a database, depending on the business scenario.

Further, the front end can have its own database, and whoever is closer to the database will have more say. By intervening in server programming, we can not only improve the front-end capabilities, but also enhance the right of the front-end in the entire software production process.

3. We want code split automation

The so-called code split is to split a large JS file (bundle) into several smaller JS files (chunk) during packaging and load them as needed at runtime. In the React ecosystem, either using lazy() + dynamic import as provided by the React package, or using the third-party @loadable class library, both have two disadvantages:

  • The code split process requires manual coding to indicate where the split point is, for example:
// PhotoRenderer.js
// NOTE: *before* Server Components

import React from 'react';

// one of these will start loading *when rendered on the client*:
const OldPhotoRenderer = React.lazy(() = > import('./OldPhotoRenderer.js'));
const NewPhotoRenderer = React.lazy(() = > import('./NewPhotoRenderer.js'));

function Photo(props) {
  // Switch on feature flags, logged in/out, type of content, etc:
  if (FeatureFlags.useNewPhotoRenderer) {
    return <NewPhotoRenderer {. props} / >; 
  } else {
    return <OldPhotoRenderer {. props} / >; }}Copy the code

As above, we need to manually code to “wrap” two layers of our original build to complete a component’s code split.

  • The current code split load on demand is also instant load, that is to say, only when the code cut point is loaded, we will immediately load the cut code. There is still a loading wait problem, which reduces the performance benefit of code split.

Server Componen, proposed and initially implemented by the React core team, can help us solve the above two problems.

  • First, we implement automatic code split by distinguishing between the Server component and the client component. By default, all import to client Component is used as the code cutting point, and the react-server-dom-webpack class library is used to do the code cutting.
// PhotoRenderer.server.js - Server Component

import React from 'react';

// one of these will start loading *once rendered and streamed to the client*:
import OldPhotoRenderer from './OldPhotoRenderer.client.js';
import NewPhotoRenderer from './NewPhotoRenderer.client.js';

function Photo(props) {
  // Switch on feature flags, logged in/out, type of content, etc:
  if (FeatureFlags.useNewPhotoRenderer) {
    return <NewPhotoRenderer {. props} / >;
  } else {
    return <OldPhotoRenderer {. props} / >; }}Copy the code

As mentioned above, we can write code without introducing code split as usual. Code split is automatically implemented by a framework or tool that integrates the Server Component.

  • Second, because we can intervene in the rendering of the Server Component, we can predict the user’s next action based on the context of the current request to pre-load the corresponding JS code. With preloading, we can solve the second problem mentioned above.

4. We want to avoid the performance burden of high abstraction

Instead of designing a new set of template syntax, React embraces javascript almost 100%. Therefore, we can use function composition and function Reflection to create powerful UI abstractions. However, excessive abstraction can introduce excessive code and runtime overhead. If a UI framework is written in a static language, then it should be possible to use AOT compilation to strip away the abstraction layer. Because javascript is a dynamically weakly typed language, this doesn’t work very well for it.

The React core team tried to work with the Prepack team to optimize the code size of the React application using AOT technology, but failed. Specifically, the React core team realized that many AOT optimizations didn’t work properly because they either didn’t have enough context for the global aspect, or they had too little. For example, a component might actually be static because it always receives a constant string prop from its parent, but the compiler can’t see that far and thinks the component is dynamic. Even if we could optimize it, the optimized code would not be debuggable or maintainable. This kind of unreliability is not acceptable to us as developers.

The React Server Component strips the abstraction layer on the server by real-time compilation and rendering on the server, reducing the performance overhead of the abstraction layer when the client is running. For example, if a component is wrapped in many layers of wrappers for configurable rows. But in fact, this code just ends up rendering as a

. To transform this component into a Server Component, we simply return a

string to the client. In the following example, we can greatly reduce the network transfer resource size and client runtime performance overhead by transforming this component into a Server Component:

// Note.server.js
/ /... imports...

function Note({id}) {
  const note = db.notes.get(id);
  return <NoteWithMarkdown note={note} />;
}

// NoteWithMarkdown.server.js
/ /... imports...

function NoteWithMarkdown({note}) {
  const html = sanitizeHtml(marked(note.text));
  return <div . />;
}

// client sees:<div> <! -- markdown output here --> </div>Copy the code

5. We want a unified technical solution for front and rear end fusion

At the beginning of the Web, we were thin clients. But with the increasing demands for interactive experiences and the advent of Ajax technology, we have entered the era of fat clients. Today, we realize that neither thin client nor fat client can meet the interactive, user experience and performance requirements of today’s Web applications. As a result, the industry began to explore the technical solution of front – end convergence. At present, there are BFF, SSR, GraphQL, etc., and some people even fall back to pure server-side rendering. In this context, the React core team wanted to unify the front-end and back-end fusion solutions. This is where the Server Component comes in.

The React Server Component technical solution takes full advantage of client and server rendering while providing a flexible customization space based on component granularity. We can decide the ratio of client component to server component by combining our own business scenario, so as to switch between [thin client], [fat client], and [neither fat nor thin client]. I call the React Server Component solution a “loose” front-end and back-end fusion solution.

At the same time, with nodeJS, we can do both client-side and server-side programming in javascript. The benefits (code reuse of the same logic, ecological Commons, zero mental burden of cross-programming, etc.) I won’t repeat here. Writing here, I see the future of React: meteor. Js.

summary

The React Server Component brings the following benefits to the front end:

  • Smaller front screen loading volume
  • More powerful data processing capabilities
  • Code automatically split
  • Preloading of JS Chunk
  • A unified and flexible solution for front and rear end fusion

How does it work?

The process loaded for the first time

The following uses the official Demo as a research sample to explore how the React Sever Component works.

I’ve divided the initial loading of an application into two phases:

  • The service start
  • The runtime

The service start

The command to start the service is NPM run start. The start command corresponds to:

$  concurrently \"npm run server:dev\" \"npm run bundler:dev\"
Copy the code

From the above command, we can see that the service startup command [parallel] does two things:

  • npm run server:dev– Start the Express server. By reading the source code (api.server.js), we can see that there are two main things in starting an Express server:

1) Register event handlers for API routes. In Demo, the following routes are available:

1. app.get('/',xxx)
2. app.get('/react',xxx)
3. app.post('/notes',xxx)
4. app.put('/notes/:id',xxx)
5. app.delete(/notes/:id',xxx)
6. app.get('/notes',xxx)
7. app.get('/notes/:id',xxx)
8. app.get('/sleep/:ms',xxx)
Copy the code

The most interesting route is the /react route.

2) Set the build and public directories to the root directory of the static resource. Note that in the Express framework, there is a priority. That is, when a client requests a static resource from the server, the first directory is given priority. Such as:

app.use(express.static('build'));
app.use(express.static('public'));
Copy the code

In the above code, static resources requested by the client are first found in the build directory, and then in the public directory if they cannot be found.

  • npm run bundler:dev– Package the client code

By looking at the package script./ SRC /scripts/build.js, we can clearly see that this is a package and build of client code:

// build.js
entry: [path.resolve(__dirname, '.. /src/index.client.js')].// index.client.js is the entry file for the client code
output: {
      path: path.resolve(__dirname, '.. /build'), // The packaged main.js is placed in the build directory.
      filename: 'main.js',}Copy the code

At the same time, we also know that the packaged static resources are stored in the Build directory. It is worth noting here that the client code is packaged using a webpack plugin called ReactServerWebpackPlugin. This plugin is very important. Because we would only produce main.js and index.html by simply packaging the module dependency tree by tracking the entry file of the client code. But when the project is actually running, we’ll also see some files like clientxxx.main.js in the build directory:

So how do they get packaged? Answer: “they are packaged by webpack using the ReactServerWebpackPlugin plugin when webpack packages the client code.” More specifically, is iterates through all of the project the client component files (because we put the form such as XXX. Client. Js | JSX | ts file agreement for client component), pack out corresponding to the chunk files. Try adding a test.client.js file to the SRC directory:

// ./src/Test.client.js
import React from 'react';

export default function Test(){
    return (<h1>I am a client component</h1>)}Copy the code

If you re-execute the package command, you will see a new client7.main.js file in the build directory.

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["client7"] and {/ * * * / "./src/Test.client.js":
/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/Test.client.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ *! exports provided: default */
/ * * * / (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default".function() { return Test; });
/* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/ *! react/jsx-dev-runtime */ "./node_modules/react/jsx-dev-runtime.js");
/* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/ *! react */ "./node_modules/react/index.js");
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);

var _jsxFileName = "/Users/samliu/dayuwuxian/server-components-demo/src/Test.client.js";

function Test() {
  return /*#__PURE__*/Object(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__["jsxDEV"]) ("h1", {
    children: "I am a client component"
  }, void 0.false, {
    fileName: _jsxFileName,
    lineNumber: 4.columnNumber: 13
  }, this);
}

/ * * * /}}));//# sourceMappingURL=client7.main.js.map
Copy the code

That’s right, this is the package we added to the test.client.js file. These chunk files of the client Component are distinct from the main.js client code. Because the client code gets the index.html request from the

So let’s conclude. That is, NPM run bundler:dev does two things:

  • Package client code (with/SRC /index.client.js as entry file)
  • Package all client Component files. If clent Component files are referenced by client code, they are packaged in main.js. In the meantime, a separate package of clientxxx.main.js will still be available.)

The packed JS code is placed in the build directory along with the index.html template file, and other static resources such as styles are placed in the public directory, all ready to be requested by the browser when the application runs.

The runtime

Without saying a word, from the perspective of the browser and the server, the first screen loading sequence of the official Demo is as follows:

The following is a brief introduction to the technical implementation details of each link:

  1. The browser initiates a/request. When the user enters the URL-http://localhost:4000/ in the address bar of the browser, the browser sends a request with path (/) to the Express server. During the service startup phase, we have already registered the corresponding handler:

    app.get('/',handleErrors(async function(_req, res) {
      await waitForWebpack();
      const html = readFileSync(
        path.resolve(__dirname, '.. /build/index.html'),
        'utf8'
      );
      // Note: this is sending an empty HTML shell, like a client-side-only app.
      // However, the intended solution (which isn't built out yet) is to read
      // from the Server endpoint and turn its response into an HTML stream.res.send(html); }));Copy the code
  2. The Express server returns the index.html file of the build folder to the browser. Index. HTML was packaged during the service startup phase, so I won’t go over it here.

  3. The browser receives the index.html file, parses it, and builds a DOM tree. In this process, it encounters two tags:

    <link rel="stylesheet" href="style.css" />
    <script src="main.js"></script>
    Copy the code

As a result, the browser will request style.css (style.css is in the public directory) and main.js.

  1. The Express server returns stye.css and main.js. When the browser executes main.js, it makes a /react request to the Express server. Main.js is the client code that is packaged and built, so let’s follow the packaged entry file to see where the source code for the /react request is. In the source code for the root.client.js
    component:

    function Content() {
      const [location, setLocation] = useState({
        selectedId: null.isEditing: false.searchText: ' '});const response = useServerResponse(location);
      return (
        <LocationContext.Provider value={[location, setLocation]} >
          {response.readRoot()}
        </LocationContext.Provider>
      );
    }
    Copy the code

    We can continue tracing back to the declaration of the useServerResponse function. With code navigation, we finally jump to the cache.client.js file, where the useServerResponse function is declared as follows:

    export function useServerResponse(location) {
        const key = JSON.stringify(location);
        const cache = unstable_getCacheForType(createResponseCache);
        let response = cache.get(key);
        if (response) {
          return response;
        }
        response = createFromFetch(
          fetch('/react? location=' + encodeURIComponent(key))
        );
        cache.set(key, response);
        return response;
     }
    Copy the code

    At this point, we can see that our “/react” request is initiated by calling the native fetch method (which, of course, is wrapped with Monkey Patch and the source is in the index.html file).

  2. After receiving the/React request, the Express server enters the corresponding route handler:

    // api.server.js
    
    app.get('/react'.function(req, res) {
      sendResponse(req, res, null);
    });
    
    function sendResponse(req, res, redirectToId) {
      const location = JSON.parse(req.query.location);
      if (redirectToId) {
        location.selectedId = redirectToId;
      }
      res.set('X-Location'.JSON.stringify(location));
      RenderReactTree -> pipeToNodeWritable -> createRequest -> resolveModelToJSON
      // Stream response
      renderReactTree(res, {
        selectedId: location.selectedId,
        isEditing: location.isEditing,
        searchText: location.searchText,
      });
    }
    
    async function renderReactTree(res, props) {
      await waitForWebpack();
      const manifest = readFileSync(
        path.resolve(__dirname, '.. /build/react-client-manifest.json'),
        'utf8'
      );
      const moduleMap = JSON.parse(manifest);
      pipeToNodeWritable(React.createElement(ReactApp, props), res, moduleMap);
    }
    Copy the code

    Here we see three key lines of code:

    const {pipeToNodeWritable} = require('react-server-dom-webpack/writer');
    const ReactApp = require('.. /src/App.server').default;
    
    pipeToNodeWritable(React.createElement(ReactApp, props), res, moduleMap);
    Copy the code

    Let’s not dive into these three lines of code. For now, all we need to know is that it does one thing in general: render the server component
    to generate a JSON description of the UI. Let’s look at the data structure of this JSON-like data:

    This data is essentially an ordinary string formatted with the newline character \n. Each row is an independent data unit that follows a common data structure:

    `${data type}${data unit ID}:${parsed JSON sequence}`
    Copy the code

    The data type

    The react-server-dom-webpack library implements processFullRow(), which parses json strings of the class:

    function processFullRow(response, row) {
      if (row === ' ') {
        return;
      }
    
      var tag = row[0]; // When tags that are not text are added, check them here before
      // parsing the row as text.
      // switch (tag) {
      // }
    
      var colon = row.indexOf(':'.1);
      var id = parseInt(row.substring(1, colon), 16);
      var text = row.substring(colon + 1);
    
      switch (tag) {
        case 'J':
          {
            resolveModel(response, id, text);
            return;
          }
    
        case 'M':
          {
            resolveModule(response, id, text);
            return;
          }
    
        case 'S':
          {
            resolveSymbol(response, id, JSON.parse(text));
            return;
          }
    
        case 'E':
          {
            var errorInfo = JSON.parse(text);
            resolveError(response, id, errorInfo.message, errorInfo.stack);
            return;
          }
    
        default:
          {
            throw new Error("Error parsing the data. It's probably an error code or network corruption."); }}}Copy the code

    There are four types of data:

    • M-client Component Chunk reference information. Client Component chunk is the chunk that webpack(more accurately, react-server-dom-webpack) packages by going through all the client Component files when we start the service.
    • The j-Server Component renders the result on the server side. The result is a react Element formatted string.
    • Suspense Component.
    • E– Error information that occurred during server rendering. Its contents are parsed by the React-server-dom-Webpack library, and the component’s FallbackComponent(<Error />Component) is displayed.

    Data unit ID

    The data unit ID is incremented from 0 by the React-server-dom-webpack as the server “renders” the entire React tree from top to bottom.

    <div className="main">
      <section className="col sidebar">
        <Test name="sam liu" />
        <section className="sidebar-header">
          <img
            className="logo"
            src="logo.svg"
            width="22px"
            height="20px"
            alt=""
            role="presentation"
          />
          <strong>React Notes</strong>
        </section>
        <section className="sidebar-menu" role="menubar">
          <SearchField />
          <EditButton noteId={null}>New</EditButton>
        </section>
        <nav>
          <Suspense fallback={<NoteListSkeleton />} ><NoteList searchText={searchText} />
          </Suspense>
        </nav>
      </section>
      <section key={selectedId} className="col note-viewer">
        <Suspense fallback={<NoteSkeleton isEditing={isEditing} />} ><Note selectedId={selectedId} isEditing={isEditing} />
        </Suspense>
      </section>
    </div>
    Copy the code

    First,
    is a server Component, so we get a key of type [J], whose ID is 0, which adds up to [J0]. Whenever a component of type host is encountered, the parse result is placed on the string value corresponding to [J0]. Then, further down the road, we see the
    component I added. Since it is a client component, we get a key of type [M] whose ID is incremented by 1 over 0 to get 1, which adds up to M1. Further down, we get
    , which is the client component, and we also get a key of type [M] whose ID is incremented by 1 to get 2, which adds up to [M2], followed by a component traversal, and so on…..

    It’s worth pointing out that since server Components can be nested with client components, in the React Element string that is the result of server Component parsing, You’ll see placeholders for these Client Components: “@”, and for Suspense Component: “$” (followed by a numeric ID). Such as:

    J0:["$"."div".null, {"className":"main"."children": [["$"."section".null, {"className":"col sidebar"."children": ["$"."@ 1".null, {"name":"sam liu"}],... ]Copy the code

    The react-server-dom-webpack browser is told to load the client Component chunk with chunk ID 1 and “render” it in the browser. Finally fill in the “render” result (the React Element string) here. Similarly to the placeholder for the Client Component, the “$+ number” in the React Element string is the UNIT IDS of Suspense Component data in the JSON class, which I won’t bother to detail.

    Parseable JSON sequence

    In jSON-like data, every row except the key composed of data type + data unit ID can be called as the key value. The value is a string that can be parsed by the json.parse () method. Don’t believe it? We can parse the data in the screenshot above:

    Parse M1:

    Parse J0:

    As you can see, J* is a react Element formatted string. Instead of a real React element, it uses an array of four array elements to represent a React elment. Here the specific format will not be explored in depth, later free to discuss.

  3. When the browser receives jSON-like data from the server, it parses it using the React-server-dom-wepack browser runtime. In the source code, there are several lines of code to support this:

    // cache.client.js 
    import {createFromFetch} from 'react-server-dom-webpack';
    
    export function useServerResponse(location) {
      / /...
      response = createFromFetch(
        fetch('/react? location=' + encodeURIComponent(key))
      );
       // ...
    }
    Copy the code

    As you can see, the jSON-like string returned by the server is taken up by the createFromFetch() method provided by the React-server-dom-Webpack browser runtime. The result of the createFromFetch() method call is a Response object that places the initial parse results on the _chunks field of the Response object:

    We then call the readRoot() method of the Response object to get the actual React Element. Finally, use the render function react-dom to render it into the browser DOM structure. The whole process can be represented by a graph:

    If you want to dig into the details of the parse process from JSON-like data to a real React Element, you’ll no doubt need to take a look at the source code for the React-server-dom-WebPack browser runtime. There is no room for further exploration.

  4. If the React-server-dom-WebPack parses JSON-like data and encounters a chunk of client component that the browser has not loaded before, the browser will initiate a request to the server for relevant resources. In this Demo, for example, is the client4. Main, js, client0. Main. Js and client5 main. Js, etc. For a client Component, once it is nested by the Server Component, it is rendered on the server side and separated into two parts: one part is the static Client Component chunk, and the other part is the dynamic props. The static part is prepared (packaged) during service startup, and the dynamic part is placed in JSON-like data. A static client Component chunk has a caching mechanism, that is, once requested, it will not be requested again, otherwise it will send a request to the server. The dynamic part changes dynamically as the server renders the result. The core of the Client Component Chunk is a JS function that returns a React Element. The mental model of the parsing process of client Componnet in JSON-like data can be summarized as follows:

    // The js functions come from the client Component chunk, and the dynamic props come from the react Element string
    // The result of the js function (dynamic props) is react elment
    // Finally put react elment back where the "@" placeholder was.Js functions (dynamic props) => React Element =>"@"A placeholderCopy the code
  5. The server returns the prepackaged Client Component chunk file.

Interface Update Process

In the case of the Demo provided by the official body, the interface update process is not much different from the first screen loading process. In other words, the browser will still have to issue a/React request to update the interface. Every time a server receives a/React request, it “renders” the entire Server Component from the root node based on the new request parameters, producing JSON-like data and sending it back to the client.

The difference is on the browser side. It is mainly reflected in two aspects:

  • Browsers cache client Component chunk requests. Therefore, when the browser parses JSON-like data during the interface update process, if a client Component chunk has already been downloaded, the browser will not make another request for that chunk.

  • In the interface update phase, there is a behavior called “reconciles” in the process of parsing JSON-like data. This behavior does not exist during the first screen loading phase. The concept of “coordination” should be similar to that proposed by React during the CSR period. The goal is to minimize DOM manipulation while preserving UI states related to the interface, such as focus states for input boxes, CSS Transitions animation states for page elements, and so on. With the introduction of the Server Component, react applications also retain the full state of the UI during interface updates, which is a key reason why the Server Component implementation returns JSON-like data rather than HTML fragments.

summary

Key words:

  • Dynamic and static separation
  • It’s still client rendering essentially
  • Middle layer thought

Now that the two phases have been sorted out, let’s summarize the basics of server Component implementation.

The rationale for the Server Component is to move component rendering from the client to the server.

Note that component rendering refers to the process of evaluating the JSX as a virtual DOM(or React element).

We can compare a scenario where the data source of component state is a server-side API. In this scenario, if the rendering of the component is done by the client, the process looks like this:

If the component is rendered by the server, the process looks like this:

In contrast to the client-side component rendering, the server component rendering result is not a real React Element, but a JSON-like data that describes the UI interface. This work is done by the React-server-dom-WebPack server runtime. Because diff operations are react Element based, React uses the React server-dom-Webpack browser runtime to convert JSON-like data to react Element.

In order to achieve the goal mentioned in background, React adds the middle layer of React-server-dom-webpack to implement a client + server coordinated rendering protocol. Server Component is still a client-side rendering technology in terms of whether it returns a complete HTML document as a criterion.

This is the basic principle of the Server Component. The more detailed principle is definitely hidden in the middle layer of the React-server-dom-Webpack. How react builds jSON-like strings from JSX on the server and how react parses JSON-like strings on the client will be discussed later.

What’s the difference

Server Component compared to SSR

To put it simply, SSR is to render the components corresponding to the first screen into an HTML markup, and then plug it into the template to form a complete HTML document and then return it to the browser. The Server Component renders the component as a React custom JSON-like string and returns it to the browser, where the React runtime parses and continues rendering. As mentioned above, the Server Component is essentially a client-side rendering technology. In addition, it is officially mentioned that we can also convert JSON-like strings to HTML markup. In this sense, the Server Component technology can be a subset of SSR technology.

Server Component compared to CSR

Note that the term component rendering mentioned below refers to the compile work from JSX to react Element.

For the Web platform, the so-called client-side rendering technology refers to the page content completion relies on javascript to generate. In this sense, the Server Component is essentially the same as a CSR. However, from the perspective of the interaction between the browser and the server, THE CSR application gets back [BUSINESS data in JSON format] from the server side, and then all the component rendering is completed in the browser side. The application in the Server Component framework gets [JSON-like UI description data] back from the server. Components are divided into client Component and Server Component. Client Component rendering is done on the browser side, and Server Component rendering is done cross-side (the server side compiles JSX to jSON-like strings, and the browser side compiles JSON-like strings to react Element).

Server Component compared to pure server rendering

Pure server rendering refers to JSP, the rendering technology of the PHP era, every time the browser makes an HTTP request, the server will return a complete HTML document, and the browser will go through the HTML file parsing process again. Obviously, this is the original page rendering technique and the purest server-side rendering. Server Component is not a pure server-side rendering technology, but a client-side rendering technology.

Compare the Server Component to GraphQL

GraphQL was originally invented as a type safe query and aggregation of back-end apis across language boundaries. Server Component refers to a page rendering technique. They are not technologies of the same dimension, but two technologies that can be used together. For example, inside Facebook, they use relay, GraphQL, and server Component together.

How to apply

Client Component or Server Component?

In the Server Component architecture, react applications are composed of client components and server components. Under this architecture, the first decision we face is which components are implemented as client Components and which as Server components. When a React Component meets one of the following conditions, we can implement it as a Server Component:

  • The component participates in the first screen rendering, and the corresponding render result is static content;
  • Components rely on large third-party libraries for internal calculations;
  • Excessive network round-trips within components to obtain business data;
  • There are a lot of layers of abstraction within the component, which means a lot of non-rendered child components.

Rules to follow when writing component code

Depending on the environment (browser and server) in which the client and Server Components are running, there are rules to follow when writing the corresponding component code.

For server Components, follow the following rules:

  • State cannot be used. Because only server Component rendering can be triggered and executed once by a client request, the Server Component has no internal state to speak of. Therefore, we cannot useState related hooks such as useState() and useReducer;

  • You cannot use life-cycle dependent effects. UseEffect () and useLayoutEffect() are disallowed;

  • Do not use browser-only apis such as DOM, CSSOM, etc.

  • You cannot use custom hooks based on state or effect.

  • Server Components can nest client Components

    Note that nested client Components can use state or effect: The client Component is nested, but it will not be rendered on the server side. It will only leave a slot in the server Component rendering result, which will eventually be rendered on the client side and backfilled into the slot.

  • The Server Component can access local databases, internal (micro), file systems, and so on.

For the Client Component, follow the following rules:

  • Client Component cannot [import] and use [real] Server Component in JSX. But you can [nest] server Components. Of course, this nesting is conditional. If a client Component is nested by the Server Component, another Server Component can be nested as children by the client Component. For example, the following code is valid:

    <ServerApp>
      <ClientTabBar>
        	<ServerTabContent />
      </ClientTabBar>
    </ServerApp>
    Copy the code

    Unlike the example above, the following code is illegal:

      // App.server.js
      <ServerApp>
        <ClientTabBar />
      </ServerApp>
      
      // ClientTabBar.client.js
      import ServerTabContent from './ServerTabContent.server.js'
      
      function ClientTabBar(){
      	return (<div><ServerTabContent /></div>)}Copy the code

    The react Component named “xxx.server.js” should not be called a server Component. Instead, the react Component should have code that accesses server capabilities. If yes, call it a “true Server Component.” Otherwise, it is not.

  • The client Component cannot access the API that is only available to the server.

  • Client Component can use state;

  • Client Component can use Effects;

  • The client Component can use custom hooks based on state and Effects.

  • The Client Component can use browser-specific capabilities;

    A Client Component is a regular React component before introducing the concept of a Server component. Therefore, the above rules for client Components are obvious and easy to understand.

pending

The fundamentals of the Server Component are set, but there are a lot of technical details and ecosystem integration work in progress. For example, the RFC lists the following:

  • Developer Tools
  • Routing
  • Bundling
  • Pagination and partial refetches
  • Mutations and invalidations
  • Pre-rendering
  • Static Site Generation

Refer to the RFC’s Open Areas of Research section for details.

conclusion

In general, when we talk about react Server Component only, it is the same as the current mainstream CSR, which essentially belongs to the client rendering technology based on javascript. However, compared with the current mainstream CSR, it has the following advantages:

  • Smaller front screen loading volume
  • More powerful data processing capabilities
  • Code automatically split
  • Preloading of JS Chunk
  • A unified and flexible solution for front and rear end fusion

If you look at one of these in isolation, maybe other technical solutions can do the same. What makes the React Server Component unique is its overall advantage of being able to solve all problems at once with one technical solution. More importantly, it is not mutually exclusive with other technologies, but can be combined to form a more complete and powerful framework.

Of course, the React Server Component isn’t a silver bullet. It solves the problem, but introduces other problems, such as higher maintenance costs due to the complexity of the environment for cross-end development, and higher understanding costs due to the addition of an intermediate layer. Therefore, we need to treat and adopt the React Server Component dialectically according to specific business scenarios.

However, the React Server Component still has a lot of technical details and surrounding ecological integration work that is not fully determined. What if it integrates with another ecosystem (e.g., next.js) a complete framework for multi-business scenarios (support for CSR,SSR, static web generation, etc.) and becomes the industry standard? When it does, it could be the silver bullet in the React front end (because it’s all we need).

In any case, the React Server Component will be a milestone in the front-end community, a sign that the boundaries of the front-end job have expanded to include server-side programming. Meanwhile, here’s the thing: Maybe react. Js is still a library, but React is already a framework.

The resources

  • Introducing zero-bundle-size React Server Components;

  • RFC: React Server Components;