The content of this article is based on the project front-end framework optimization and extraction of some practical single page optimization tips, in this precipitation, I hope to help students groping in this aspect.

The front-end framework is built on React 16+, React – Router V4, WebPack V3, and Next1.x. Next is alibaba Group e-commerce universal UI component library (similar to Antd). Let’s start with some common one-page application optimization tips.

Routing design

Single-page applications, as the name suggests, have just one HTML document stored on the server, and once the server spits out the document, everything else happens in the front end (again, the data is retrieved via an Ajax asynchronous call interface, of course). How do I ensure that when I switch pages, the page path changes without refreshing the page?

Our traditional approach is to put the page routing design in the back end, so that a page path is requested, the server returns the document, and the browser refreshes. This is fine for normal apps, but not for single-page apps, because if the frame of the page is fixed (headers, sidebars), refreshing the page every time to change what’s in it can lead to a bad user experience. So, routing design has to go to the front.

The front end dynamically switches sub-pages, one way is by changing anchor points. Front-end frameworks such as www.taobao.com/#a and www.taobao/com/#b switch pre-set pages by recognizing anchor points. This seems fine on a functional level, but if you’re a bit ambitious, you probably won’t like this route because it’s not semantic and intuitive. For example, if I wanted to add a “New category” page, you’d think of /category/add, not #addCategory.

So how do you change the page path without refreshing the page (request server)? Check out HTML5’s History API. In short, the API’s pushState and popState are used to artificially manipulate the browser history.

Well, at this point, you’re probably enjoying the seamless switching of pages. Then accidentally hit refresh, GG. Why did you just hang up? That’s because you didn’t go to the server before the browser forward or back, but the page refresh will still go to the server request, this time there is no design of the corresponding path route, of course, hang. So, in order to fix this problem, the server should do something logical, which is to return the unique HTML document for all unmatched routes. What about the 404? Let the front end take care of it. Or, more conveniently, do the following configuration directly in the Nginx layer:

location / {
  try_files $uri /index.html;
}
Copy the code

Code Split & Loading transition

Once the routing structure is built, we start writing JS (bugs) like crazy. When you think you can go home from work, finally pack up the front-end resources, GG. You silently tell yourself, a JS 3M, Gzip pressure is estimated to be 900K, now the network speed is so fast, the problem is not big. As you get ready to stand up, you remember why you started out front. Pour a cup of hot water, continue to work overtime.

In the weak network environment, the loading of 900K JS still takes 2-3 seconds, and the user’s page will be blank. Therefore, we want to split this JS, can first load the page frame required part of the general JS code, and then load the specific page JS? That’s what Code Split does. The traditional Code Split is to Split the common JS in multiple separate pages, and then load the common + separate JS in each page. This approach won’t work in a single page application.

For single-page apps to do Code Split, you need to use dynamic import. That is, the page only loads the initial JS (JS framework + generic components), and then loads the specific JS code according to the specific page. Ensure can be implemented using require.ensure, and webpack2.4 can be implemented using the so-called magic comment: import(/* webpackChunkName: “my-chunk-name” */ ‘module’); .

For a friendly experience, you should have a Loading animation transition before the page js is fully loaded. To quickly implement both of these, you can use an existing full-fledged solution called React-loadable.

Once the above two things are implemented, it is no problem to test in the local environment, and then published online, GG, mental collapse. You found that the split JS bundle was loaded in the wrong path: The domain name of the page is www.taobao.com, the address of the main bundle is g.alicdn.com/a/build123, and the address of the loaded bundle is really www.taobao.com/build456. After drying the tears from your eyes, you open the famous stackoverflow network, stackoverflow. After consulting the resources, you find that the loading path of the resource is related to the publichPath path in webPack, so you modify the WebPack configuration for online and re-publish online. After seeing JS loading normally, I shed tears of emotion.

Reducer Dynamic injection

Because the project is based on Redux to do application state control. Redux is long-winded, but the standard agreement is good for team collaboration (Redux Saga is also considered, but the project time is tight, so outsourcing students to study temporarily costs a lot of time). If the main bundle load the action reducer of all the pages from the beginning, the main bundle becomes large again. And to optimize performance, you don’t want the root state initialized by the project to contain the initial state of all the pages. You want the effect of injecting the root state after a child page is loaded. We have the following Reducer registration code, which uses publish and subscribe mode:

export class ReducerRegistry { constructor() { this._emitChange = null; this._reducers = {}; } reducer getReducers() {return {... this._reducers }; Var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var reducer) { this._reducers = { ... this._reducers, [name]: reducer }; if (this._emitChange) { this._emitChange(this.getReducers()); } } setChangeListener(listener) { this._emitChange = listener; } } const reducerRegistry = new ReducerRegistry(); export default reducerRegistry;Copy the code

Then listen for the injection event in the main bundle and replace the Store Reducer:

reducerRegistry.setChangeListener(reducers => {
    store.replaceReducer(combineReducers(reducers));
});Copy the code

Inject reducer into the child page:

reducerRegitry.register("pageName", reducer);Copy the code

Above, the reducer dynamic injection function is completed.

For more tips, please leave a comment.