06 How to Achieve Dynamic Loading Through Modular Asynchronous and Observers

06 How to Achieve Dynamic Loading Through Modular Asynchronous and Observers #

Hello, I’m Ishikawa.

In the previous sections of the functional programming course, we learned that in functional programming, side effects usually come from outside the function and often occur during the input process. This is actually viewed from a spatial perspective.

In today’s lesson, we will look at events in asynchronous programming from a temporal perspective and see how they can cause side effects, as well as how to manage these side effects.

How to handle time status in asynchronous events? #

In functional programming, when discussing asynchronous events, we often mention the concepts of trustable and promises. This concept actually originates from contracts or covenant law, and it is not limited to classic contracts. It also applies to concepts such as smart contracts, where the underlying logic is based on contracts and consensus.

So why do we need this concept when dealing with asynchronous events? Let me show you some of the problems that programs encounter when dealing with async events.

Suppose we have two functions, getUser and getOrders, which retrieve user information and order information respectively, based on a user ID. If getUser receives a response first, it won’t be able to get the order information. Similarly, if getOrders receives a response first, it won’t be able to get the user information.

This creates a race condition between the two functions.

var user;

getUser(userId, function onUser(userProfile) {
    var orders = user ? user.orders : null;
    user = userProfile;
    if (orders) {
        user.orders = orders;
    }
});

getOrders(userId, function onOrders(userOrders) {
    if (!user) {
        user = {};
    }
    user.orders = userOrders;
});

As shown in the diagram, no matter who arrives first, there will be a problem. You might suggest separating the processing of these functions, but that would eliminate parallelism and result in serial execution, which usually takes more time.

In asynchronous events, time represents state. In synchronous operations, we don’t need to consider time, but in asynchronous operations, time comes into play. Time is not just state, it is also the most difficult state to manage.

What does this have to do with trust and promises? It is because time is what separates trust and promises! To illustrate this, let’s use the example of a contract transaction. In synchronous transactions, it is like handing over money and receiving goods at the same time. But in asynchronous transactions, we can only rely on time to prove whether the promises have been kept. That’s why in JavaScript, the tool for solving asynchronous problems is called a promise.

But why should we trust a promise? In real-life transactions, people usually sign contracts and rely on time to prove if promises have been kept. In JavaScript functional programming, the solution is to get rid of the hassle of contracts. To make you trust my promise, let’s just eliminate time altogether.

Yes, that’s how cool it is. The way to eliminate time is to handle async events in a synchronous way. Here’s an example:

var userPromise = getUser(userId);
var ordersPromise = getOrders(userId);

userPromise.then(function onUser(user) {
    ordersPromise.then(function onOrders(orders) {
        user.orders = orders;
    });
});

This way, even if the user and order information are retrieved in parallel, we can still update the order information to the user object using the then method in the order of synchronization.

How to handle time state in loop events? #

In functional + reactive programming, besides network events, there are more examples where time is removed, such as loop or user events, which can be handled in a synchronous manner to process asynchronous events.

For example, let’s say we have two objects, a producer and a consumer. The consumer wants to map changes that occur in the producer. If the producer is “hardworking” and produces in real-time, the consumer can consume in real-time as well.

// Hardworking producer
var producer = [1,2,3,4,5];

// Consumer
var consumer = producer.map( function triple(v){
    return v * 3;
} ); // [3,6,9,12,15];

But what if we have a lazy producer and the consumer doesn’t know when the producer will change in the future? In this case, we need to treat the lazy producer as an observed object and react whenever it changes. This is the combination of “asynchronous pattern in functional programming” and “observer pattern in reactive programming”.

Here is a relatively abstract example of an asynchronous loop event. However, in reality, we encounter asynchronous events such as user input, such as mouse clicks, keyboard input, etc., which are DOM events. In such cases, we can use the concept of “laziness” to “lazy load” some content based on user response.

/* For example, using RxJS, an extension of reactive JS */

// Lazy producer
var producer = Rx.Observable.create( function onObserve(observer){
    setInterval( function everySecond(){
        observer.next( Math.random() );
    }, 1000 );
} );

// Consumer
var consumer = producer.map( function triple(v){
    return v * 3;
} );
consumer.subscribe( function onValue(v){
    console.log( v );
} );

How to handle time states in user events? #

Continuing from our discussion on reactive and observer patterns, let’s take a look at some methods used in front-end development to handle dynamic content loading on web pages. This brings us to the topic of dynamic imports.

First, let’s take a look at the sequence of events from loading to execution of a module on a web page. We can see that this sequence can be roughly divided into four steps: loading, parsing, compiling, and executing. With dynamic loading, the module continues to load based on demand after initialization.

Speaking of dynamic imports, they can be broadly categorized into two types: loading on visibility and loading on interaction.

Loading on visibility, also known as lazy loading, is often used in long pages. For example, in a product details page, the product features are usually presented step by step using storytelling and visual content, and the parameters, one-click purchase, and add-to-cart sections are displayed at the end. This means that we don’t need to load the entire page initially but only load related content when the user scrolls to a certain section.

Loading on interaction refers to loading that occurs when the user interacts with the page, such as clicking a button. For example, in some calendar applications, the calendar is only displayed and interacted with when the user performs specific actions. Such modules can be dynamically loaded.

Note that there are several important metrics to consider. In the initial loading phase, we usually focus on the First Contentful Paint (FCP) and Largest Contentful Paint (LCP), which represent the first time the page is rendered. In subsequent dynamic loading, we focus on the Time to Interactive (TTI), which occurs when the user starts scrolling below the first screen or clicks a button to open the calendar popup.

You might think that such optimizations may only save a few hundred kilobytes or a few megabytes, so are they worth it? However, every little bit counts, and when you’re developing a complex web application that requires continuous expansion of modules, these optimizations can lead to significant improvements in both quality and quantity.

At this point, we usually use bundling tools like Webpack to first load core components, render the main program, and then dynamically load specific modules based on interaction needs.

Moreover, for dynamic loading, there are many third-party libraries available to support it. One example is the Suspense feature in React. If you’re rendering on the server-side with Node.js, you can refer to libraries like Loadable Components. Of course, if you don’t want to use these third-party libraries, you can develop your own solution, but the principles are similar.

However, before diving further into dynamic loading, we should first understand two fundamental rendering modes: browser rendering and server-side rendering.

In client-side rendering (CSR) mode, we download HTML, JS, CSS, and data first, and then start rendering once everything is downloaded.

In server-side rendering (SSR) mode, we show the user a complete page without interactivity first. The relevant data is loaded and hydrated from the server before interaction can occur, such as when event handling is added to a button.

This approach may seem better than CSR, but it also has its own issues. For example, as users of some applications, we may encounter situations where a component doesn’t respond when we click a button before loading and hydration.

So how do we solve this problem in interaction-driven dynamic loading? For example, Google uses and open-sourced a tool called JSAction, which loads a portion of lightweight code first. This code can “remember” user actions, and then it loads the components based on user interactions, ensuring that the components execute the remembered user requests once they are loaded. This perfectly solves the aforementioned problem.

Summary #

Through today’s study, we have understood that in functional programming + reactive programming, time is a state, and it is the most difficult state to manage. By using the concept of promises, we can eliminate time and handle asynchronous events in a synchronous manner.

Furthermore, through the observer pattern, we can better deal with the unknown and perceive and respond through action. This loading method can reduce unnecessary and excessive resource waste when the application’s user base reaches a certain scale.

Thought-provoking question #

We mentioned that dynamic loading of events can help reduce costs and improve efficiency. Can you share your experience in designing, analyzing, and optimizing resource loading in front-end development?

Feel free to share your answer, exchange learning experiences, or ask questions in the comments section. If you find it helpful, you can also share today’s content with more friends.