26 Special Types Frontend Special Processing Loading and Rendering Patterns

26 Special Types- Frontend Special Processing Loading and Rendering Patterns #

Hello, I’m Ishikawa.

In the previous lectures, we have introduced the classic design patterns. Today, let’s take a look at some design patterns specific to JS. Starting from functional programming, we have always emphasized the importance of reactive programming in the frontend. Therefore, I believe this part can be systematically explored in three major areas: componentization, loading and rendering, and performance optimization patterns. Now, let’s delve into them.

Component-based Pattern #

First, let’s take a look at the design pattern of component-based development. I would like to first ask you to consider the following question: Why is component-based development, especially in the frontend using the React framework based on JS, considered very important?

With the gradual popularity of Web and WebGL, many desktop applications we used to use have been replaced by web applications. The reasons are that web applications can achieve similar functionality to desktop applications, save resources in downloading and storing on our phones or PCs, and allow us to access the content we need anytime, anywhere, as long as there is an internet connection and we enter a URL. In addition, in office software, it greatly enhances collaboration. For example, commonly used applications such as QQ Mail, Google Docs, or Shimo Docs can all be considered complex web applications composed of components.

Next, let’s talk about several component-based patterns that are often used in React. Here, we need to first note that component-based development in React is different from the Web Component we are usually familiar with. When we talk about Web Components, we focus more on encapsulation and reusability of components, which is more related to the classic object-oriented design pattern. React Component, on the other hand, focuses more on keeping the DOM and state data in sync through a declarative approach. In order to better achieve component-based development, React has introduced some different patterns, including Context Provider, Render Props, Higher-Order Components, and the later introduction of Hooks.

We can roughly divide these component-based patterns into two categories: the classic patterns such as Context Provider, Render Props, and Higher-Order Components used before the introduction of Hooks, and the new patterns brought by Hooks. Now, let’s take a look at the classic patterns first.

Classic Patterns #

First, let’s take a look at the Context Provider Pattern, which is a component-based approach of passing data to multiple components by creating a context. Its purpose is to avoid prop drilling, which means avoiding the cumbersome process of passing data from parent components to child components layer by layer.

So, what are some practical applications of this pattern? One example is when we want to display different content based on the current context of the application’s interface. This pattern comes in handy for scenarios such as showing different content for logged-in and non-logged-in users, applying special theme skins during holidays like Spring Festival or National Day, or adjusting content display based on the user’s country or region.

For example, suppose we have a menu that contains a list and list items. In the following code, if we pass the data layer by layer, it becomes very cumbersome.

function App() {
  const data = { ... }
  return (<Menu data={data} />);
}

var Menu = ({ data }) => <List data={data} />
var List = ({ data }) => <ListItem data={data} />
var ListItem = ({ data }) => <span>{data.listItem}</span>

However, by using React.createContext, we create a theme. Then, through ThemeContext.Provider, we can create a related context. This way, we don’t need to pass the data to each element in the menu one by one, and all elements in the context can access the relevant data.

var ThemeContext = React.createContext();

function App() {
  var data = {};
  return (
      <ThemeContext.Provider value = {data}>
        <Menu />
      </ThemeContext.Provider>  
  )
}

Through React.useContext, we can retrieve and manipulate data in the element’s context.

function useThemeContext() {
  var theme = useContext(ThemeContext);
  return theme;
}

function ListItem() {
  var theme = useThemeContext();
  return <li style={theme.theme}>...</li>;
}

After talking about the Context Provider, let’s take a look at the Render Props Pattern. First, think about why we need the Render Props Pattern.

Let’s take a look at the problems that may arise without the Render Props Pattern. For example, in the following price calculator example, we want the program to calculate the price based on the number of products entered. However, without the render prop pattern, although we try to calculate and display the calculated price based on the entered value * 188, the price calculation cannot access the value of the entered quantity, so the actual price cannot be calculated.

export default function App() {
  return (
    <div className="App">
      <h1>Price Calculator</h1>
      <Input />
      <Amount />
    </div>
  );
}

function Input() {
  var [value, setValue] = useState("");
  return (
    <input type="text" 
      value={value} 
      placeholder="Enter quantity"
      onChange={e => setValue(e.target.value)} 
    />
  );
}

function Amount({ value = 0 }) {
  return <div className="amount">{value * 188} yuan</div>;
}

To solve this problem, we can use render props, where the amount component is passed as a child element of the input component and the value parameter is passed into it. This means that through render props, we can share certain data or logic between different components through props.

export default function App() {
  return (
    <div className="App">
      ...
      <Input>
        {value => (
          <>
            <Amount value={value} />
          </>
        )}
      </Input>
    </div>
  );
}
function Input() {
  ...
  return (
    <>
      <input ... />
      {props.children(value)}
    </>  
  );
}

function Amount({ value = 0 }) {
  ...  
}

After discussing about the render props, let’s take a look at the Higher Order Components (HOC) Pattern. This term sounds similar to the higher-order functions we mentioned when we talked about functional programming. As we mentioned before, a function that takes another function as an argument and returns a function as a result is called a higher-order function. In a similar way, in higher-order components, we can pass a component as an argument and return a component.

HOC

So, what is its application? Imagine that, without higher-order components, if we want to add rounded corners to some button or text components, we may need to modify the code inside the component. However, with higher-order functions, we can wrap some methods around the original rectangular text box and button components to achieve rounded corners. In practical applications, it can play a role similar to “decorators”. It not only allows us to avoid modifying the components themselves, but also allows us to reuse the abstracted functionality and avoid code redundancy.

// Higher-order function
var enhancedFunction = higherOrderFunction(originalFunction);
// Higher-order component
var enhancedComponent = higherOrderComponent(originalComponent);

// Higher-order component as a decorator
var RoundedText = withRoundCorners(Text);
var RoundedButton = withRoundCorners(Button);

Hooks Pattern #

So far, we have seen several classic ways to help us implement and optimize componentization. Now, let’s take a look at the Hooks introduced in React 16.8.

The most direct function of Hooks is to use functions instead of the new class introduced by ES6 to create components. As we mentioned before, when we talked about object-oriented programming in JavaScript, understanding the binding of this can be confusing for developers coming from other languages. With Hooks, we can create components more intuitively through function expressions.

Let’s start with an example of a counter. If we were to create it using the traditional class syntax, we would need to use this.state, this.setState, and this.state.count to initialize, set, and read the count state.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        Clicked {this.state.count} times.
      </button>
    );
  }
}

With Hooks, we can replace this.state with useState(0) to create a count state initialized with 0. Also, when clicking the counter button, we can use count and setCount instead of this.state.count and this.setState to read and update the count state.

import React, { useState } from 'react';
function App() {
  var [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Clicked {count} times.</button>
    </div>
  );
}

In this example, you can see that we used destructuring to create two variables for the count state, count and setCount. When we assign these two values to userState(0), they will respectively be assigned as the count value and the function to update the count.

// Array destructuring
var [count, setCount] = useState(0);

// Equivalent to
var countStateVariable = useState(0);
var count = countStateVariable[0];
var setCount = countStateVariable[1];

In addition to replacing classes with functions, Hooks also allow components to be decoupled by functionality and then combined by relevancy. Without Hooks, we might need to combine functionalities using component lifecycle methods. For example, an application component may have two different functionalities: displaying the number of items in the shopping cart and displaying the availability of customer support. If we manage them using the same component lifecycle method, such as componentDidMount, it would aggregate unrelated functionalities. However, with Hooks like useEffect, we can separate unrelated functionalities and then combine them based on relevancy.

class App extends React.Component {
  constructor(props) {
    this.state = { count: 0, isAvailable: null };
    this.handleChange = this.handleChange.bind(this);
  }
  componentDidMount() {
    document.title = `The shopping cart contains ${this.state.count} items`;
    UserSupport.subscribeToChange(this.props.staff.id, this.handleChange);
  }
}

function App(props) {
  var [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `The shopping cart contains ${count} items`;
  });
  var [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleChange(status) {
      setIsOnline(status.isAvailable);
    }
    UserSupport.subscribeToChange(props.staff.id, handleChange);
  });
}

In addition to the above two benefits, Hooks also make it easier to share logic between components. As we have seen in the example above, if we want to reuse some behavior in different components without Hooks, we may need to use render props and higher-order components. However, this approach incurs the cost of changing the original structure of the components, as a large amount of providers, render props, and higher-order functions are added outside the components. This excessive abstraction leads to “wrapper hell”. With Hooks, we can achieve similar functionality in a more native way.

Loading and Rendering Modes #

Previously, when discussing responsive design, we mentioned the concepts of client-side rendering (CSR), server-side rendering (SSR), and hydration. Today, let’s take a more systematic look at loading and rendering modes. First, let’s start with rendering modes.

Rendering Modes #

When web applications first became popular, many single-page applications (SPAs) were developed using client-side rendering. This mode allows users to switch between different pages without the need for browser refresh. While this brings convenience, it also leads to performance issues. For example, the First Contentful Paint (FCP), Largest Contentful Paint (LCP), and Time to Interactive (TTI) may all take longer. To address these performance issues, techniques such as code minimization, preloading, lazy loading, code splitting, and caching can be used.

However, compared to server-side rendering, client-side rendering not only has performance issues but also affects search engine optimization (SEO). To solve this problem, some websites generate a separate set of backend pages specifically for search engine indexing on top of SPAs. But as the entry pages for search, the backend-rendered pages also suffer from a longer Time to First Byte (TTFB).

To address the issues with client-side and server-side rendering, the concept of static rendering emerged. Static rendering uses a pre-rendering approach, meaning that HTML pages that can be cached on a CDN (Content Delivery Network) are pre-rendered on the server. When the frontend initiates a request, the rendered files are sent directly to the backend. This approach reduces the TTFB. In static rendering, the JS files are usually smaller compared to the static page content, so the FCP and TTI of the pages are not too high.

Static rendering is generally referred to as static generation (SSG). From this, the concept of incremental static generation (iSSG) has emerged. While static generation works well for handling static content, such as an “About Us” introduction page, what about dynamic content, such as “blog articles”? In other words, we need to support the addition of new pages while also updating existing ones. This is where incremental static generation comes into play. iSSG allows for generating incremental pages on top of SSG and re-generating the existing parts.

Whether it’s a “About Us” page or a “blog article” page, while they can be divided into static and dynamic content from a content perspective, overall they don’t require frequent interaction. However, if the static rendered content requires behavior code to support interaction, SSG can only guarantee FCP, but it’s difficult to guarantee TTI. This is where hydration, which we mentioned earlier, comes into play. Hydration can give dynamically loaded elements dynamic behavior. When the user initiates interaction, progressive hydration comes into action.

In addition to progressive hydration during static rendering, server-side rendering can also achieve progressive rendering using streams in Node.js. With streams, the content of the page can be transmitted in segments to the frontend, allowing the frontend to load the first segments first. In addition to progressive hydration, selective hydration can utilize Node.js streams to delay the transmission of certain components, and only hydrate the parts that are transmitted to the frontend first. This approach is called selective hydration.

In addition to the above-mentioned patterns, there is another comprehensive pattern called Islands Architecture. Just like what we learned in geography class, all continents can be seen as “islands” drifting on the ocean. This pattern considers all components on the page as “islands”. It treats static components as static page “islands” and uses static rendering, while dynamic components are treated as individual widget “islands” and rendered using server-side hydration.

Loading Patterns #

To complement the rendering patterns mentioned above, a series of loading patterns are naturally required. Static content is imported statically, while dynamic content is imported dynamically. Based on the progressive enhancement approach, we can also progressively import the content that needs to be displayed after specific areas or interactions have been activated. The imported content can be loaded through bundle splitting, and components or resources can be loaded based on the path (route-based splitting).

In addition to this, another pattern worth mentioning is PRPL (Push Render, Pre-Cache, Lazy-load). The core idea of the PRPL pattern is to first push the minimal initial content for rendering during initialization. Then, using a service worker in the background, cache other frequently accessed route-related content. When the user wants to access the related content, it can be lazily loaded directly from the cache without making new requests.

So how is the PRPL pattern implemented? This is where the features of HTTP2 come into play. Compared to HTTP1.1, HTTP2 provides server push, which can push all additional assets except for the initialization resources to the client at once. PRPL leverages this feature of HTTP2. However, this feature alone is not enough because although these assets are stored in the browser’s cache, they are not in the HTTP cache. Therefore, when users visit again, they still need to make new requests.

To solve this problem, PRPL uses a service worker to precache the content pushed by the server. It also utilizes code splitting to bundle and load different components and resources based on the routing requirements of different pages, enabling on-demand loading of different content.

When it comes to loading, there is another concept that we need to pay attention to: pre-fetch does not equal pre-load. Pre-fetch mainly refers to obtaining resources in advance from the server, aiming to cache them for quick loading when needed in the future. On the other hand, pre-loading is a way to load special assets that are needed for initialization, such as special fonts, so that they can be smoothly displayed when content is loaded.

Performance Optimization Modes #

Earlier, we mentioned some componentization and rendering modes specific to JavaScript. In the loading and rendering mode, we have already seen shadows that improve performance. Now let’s look at another category: further optimizing front-end performance. Here, it is worth mentioning two optimization modes: tree shaking and list virtualization.

Tree Shaking #

One of these optimizations, tree shaking, removes unused code from the JavaScript context (dead code). Why do we need to remove this code? Because if these unused codes exist in the content loaded last, they will consume bandwidth and memory. However, if they will not be used during program execution, they can be optimized and removed.

Why is there the word “tree” in tree shaking? In fact, it is a type of graph structure, but you can imagine it as a structure similar to an Abstract Syntax Tree (AST). The tree shaking algorithm traverses the execution relationships in the entire code, and elements that are not traversed are considered unnecessary and are pruned accordingly. It depends on the import and export statements in ES6 to detect if the code module is exported, imported, and used by JavaScript files. In JavaScript programs, when we use module bundling tools such as webpack or Rollup to prepare and publish code, tree shaking algorithms are used. These tools can automatically delete unreferenced code when bundling multiple JavaScript files into one, resulting in a cleaner and lighter generated file.

List Virtualization #

After discussing tree shaking, let’s take a look at list virtualization. Where does the term “virtualization” come from? It’s a bit like the “Schrodinger’s cat” concept we mentioned in quantum mechanics, where the things in front of us are rendered only at the moment of observation. In this case, our world is like a “virtual” sandbox. In list virtualization, we only focus on the position the rendering window moves to. This can save computational power and related time.

In React-based third-party tools, there are two libraries that support list virtualization: react-window and react-virtualized. Both of these libraries are created by the same author. Compared to react-virtualized, react-window combines the tree shaking optimization mentioned above, and it is also lighter in weight. If you are interested, you can learn more on Github.

Summary #

Through today’s study, we have gained a more systematic understanding of several design patterns in responsive programming in front-end interactive scenarios. We have learned that we can reduce nesting relationships between components and establish more efficient connections between data, states, and behaviors through Hooks. In the loading and rendering patterns, we have seen how to progressively provide content and interaction in order to improve display and interaction speed and reduce resource consumption. Lastly, in the performance optimization patterns, we can see more ways to improve performance by optimizing resources and saving computing power.

Reflection Questions #

We mentioned earlier that Hooks can reduce nesting. Do you think this approach can directly replace patterns such as context providers, render props, and higher-order components (HOCs)?

Please feel free to share your answers, exchange learning experiences, or ask questions in the comments section. If you find it helpful, you are also welcome to share today’s content with more friends. See you in the next class!