preface

Rendering on the Web: Performance Implications of Application Architecture (Google I/O ’19) Performance Implications of Application Architecture (Google I/O’ 19)

  • pre-rendered
  • Isomorphism rendering
  • Streaming rendering
  • Progressive water injection (wonderful)

Application architecture system

When we talk about “application architecture,” it can be understood as building a website through a combination of the following parts.

  1. Component modelComponent model.
  2. Rendering and loadingRender and load.
  3. Routing and transitionsRouting and transitions.
  4. Data/state managementData and status management.

Performance indicators

Before analyzing the rendering performance of a page, let’s take a look at some of the more important metrics for understanding:

  1. FP: First Paint, part of the Paint Timing API, is the middle between the page navigation and when the browser renders the First pixel of the page onto the screen. Rendering is anything that is different from what was on the screen before entering the page navigation.

  2. FCP: First Contentful Paint, the First rendering of content is when the browser renders the First block of content in the DOM and gives the user feedback that the page is loading.

  3. TTI: Time to Interactive The first Time that the user can actually trigger events for DOM elements to interact with the page.

  4. FID: First Input Delay Measures the time a user First interacts with your site (that is, when they click a link, click a button, or use a custom javascript-driven control) to the time the browser is actually able to respond to that interaction.

  5. TTFB: Time to First Byte TTFB: Time to First Byte, as the name implies, refers to the Time between the client and the server to transmit data (including DNS, socket connection, and request response Time) to the client browser. It is an important indicator of the response speed of the server.

If you’re not familiar with these metrics, I’ll look at them in the context of actual use cases in the following sections.

The cost of rendering

Client-side rendering

It costs a lot to get HTML, CSS and JavaScript from the server. Take a CSR (Client rendering) website as an example. The client rendering website relies on the framework library (Bundle) and application program (APP) for initial rendering. Assuming it has 1MB of JavaScript Bundle code, the user will not see the page until this large chunk of code is loaded and executed.

Its structure is generally as follows:

Analyze its flow:

  1. Users enter the url to access the site and pull HTML resources.

  1. The bundle loaded with the script tag is found in the HTML resource and again requests to pull the bundle. It is also included in the performance statistics indicatorFPTo complete.

At this stage, the page is basically meaningless, but you can also put in some static skeleton screens or load prompts to make the user friendly.

  1. After the JavaScript bundle is downloaded and executed, the page is rendered with meaningful content. The correspondingFCPTo complete.

The user can actually interact with the page only after the framework has added various event bindings to the DOM node, which also corresponds to TTI.

The downside is that the user doesn’t see anything meaningful until the entire JavaScript dependency execution is complete.

Homostructurally rendered SSR with Hydration

Based on the above shortcomings of client rendering and users’ richer requirements for CSR application interaction, the “isomorphic rendering” combining the performance of SSR and CSR, SEO, and the advantages of data acquisition was born. To put it simply, it is:

  1. For the first request, the server side makes use of the server side rendering capability provided by the framework to directly request data in situ and generate HTML pages containing complete content. Users can see the content without waiting for the js loading of the framework.

  2. After the page is rendered, one takes advantage of the framework’s capabilities, such as the “dry” HTML registration events returned by the server, to enrich the page. With these events, one has the same rich client-side interactions as a traditional CSR.

In homogeneous applications, as soon as the HTML page is returned, the user can see a colorful page:

Once the JavaScript is loaded, the user can interact with the content (click to enlarge, jump to a page, etc.).

Code comparison

The code for a typical CSR React application looks like this:

SSR code, on the other hand, requires the cooperation of the server.

The server passes firstReactDOMServer.renderToStringSerialize the component as an HTML string on the server and return it to the front-end:

Front end byhydrateWater injection makes functional interaction complete:

The same is true for Vue SSR:

Isomorphic defects

So is isomorphism a perfect application? Of course not, in fact, normal homogeneous apps only increase the speed at which the FCP is seen by the user, but there is still a lot of interaction to be had until the framework code is downloaded, hydrate hydrate is injected and so on.

In addition, for FID, also known as First Input Delay, SSR quickly renders content, which makes it easier for users to mistakenly think that the page is interactive, but makes the time of “user’s First click-browser response event” longer.

Therefore, isomorphic applications are likely to become a double-edged sword.

Let’s discuss some of the options.

Pre-rendering.

For content that doesn’t change very often, using pre-rendering is a good way to generate static HTML pages with the framework’s ability to do so at code build time, rather than being regenerated when the user requests the page, as isomorphic applications do, allowing it to return to the page almost immediately.

Of course, there are major limitations:

  1. This applies only to static pages.
  2. You need to list the URLs that need to be pre-rendered in advance.

Streaming rendering

Streaming rendering allows the server to fragment large chunks of content so that the client does not need to receive the HTML in its entirety, but starts rendering as soon as it receives the first part, which greatly improves TTFB’s first byte time.

In React, streaming rendering can be used with renderToNodeStream:

Progressive Hydration

We know that the Hydrate process will need to traverse the entire React node tree to add events, which can take a long time with a large page, so can we just “water” the key parts, such as the visible parts of the view, and let them interact first?

Imagine the features:

  1. Progressive injection at the component level.
  2. The server still renders the entire page.
  3. Pages can be sharded to “boot” components based on priority.

Use a GIF to intuitively feel the difference between ordinary water injection (left) and progressive water injection (right) :

You can see that users can interact for the first time much earlier.

React Hydrator: React Hydrator: React Hydrator: React Hydrator: React Hydrator: React Hydrator

Let’s first look at the overall structure of the application:

let load = () = > import('./stream');
let Hydrator = ClientHydrator;

if (typeof window= = ='undefined') {
  Hydrator = ServerHydrator;
  load = () = > require('./stream');
}

export default function App() {
  return (
    <div id="app">
      <Header />
      <Intro />
      <Hydrator load={load} />
    </div>
  );
}
Copy the code

Using different Hydrators depending on the client and server environments, the server simply returns plain HTML text:

function interopDefault(mod) {
  return (mod && mod.default) || mod;
}

export function ServerHydrator({ load, ... props }) {
  const Child = interopDefault(load());
  return (
    <section>
      <Child {. props} / >
    </section>
  );
}
Copy the code

The client, on the other hand, needs to implement the key parts of progressive water injection:

export class Hydrator extends React.Component {
  render() {
    return (
      <section
        ref={c= > (this.root = c)}
        dangerouslySetInnerHTML={{ __html: '' }}
        suppressHydrationWarning
      />); }}Copy the code

First render part, using dangerouslySetInnerHTML to make this part of the initialization into empty HTML text, and because the server must be as usual full rendering content, and the client due to initialization need not do any processing, The React internal consistency check for server and client content fails.

Using dangerouslySetInnerHTML will allow React to not further hydrate through children and will simply use the HTML returned by the server render, ensuring that the render style is OK before flooding.

SuppressHydrationWarning Cancel React warning for content consistency detection failure.

export class Hydrator extends React.Component {
  componentDidMount() {
    new IntersectionObserver(async ([entry], obs) => {
      if(! entry.isIntersecting)return;
      obs.unobserve(this.root);

      const{ load, ... props } =this.props;
      const Child = interopDefault(await load());
      ReactDOM.hydrate(<Child {. props} / >.this.root);
    }).observe(this.root);
  }

  render() {
    return (
      <section
        ref={c= > (this.root = c)}
        dangerouslySetInnerHTML={{ __html: '' }}
        suppressHydrationWarning
      />); }}Copy the code

Then, during client initialization, IntersectionObserver is used to monitor whether component elements enter the view or not. Once they enter the view, the component is dynamically imported and reactdom.hydrate is used for actual water injection.

Not only is water injection dynamic, but the download of component code takes place when the component enters the view, truly “loading on demand.”

The purple animation in the GIF indicates that the progressive Hydrate is complete.

Comparing the performance of full and progressive waterflooding shows that the time of first interaction is significantly advanced:

Of course, knowing how this works, you can get hydrate not only by listening for components to enter the view, but also by timing it with hover, click, etc., which can be flexibly adjusted according to business needs.

Visit the url in the image to get your favorite framework articles on this topic:

conclusion

This paper summarizes the Rendering on the Web: Performance Implications of Application Architecture (Google I/O ’19)

  • pre-rendered
  • Isomorphism rendering
  • Streaming rendering
  • Progressive water injection

Choosing the corresponding optimization means in different business scenarios is a necessary skill for an excellent front-end engineer. I believe you will gain something after reading this article.

This article was first published on the public account “front-end from advanced to hospital”, attention can also get advanced front-end route, and front-end zero-based algorithm problem solving, I will also release some internal information, share some fun work, and chat with everyone.