There are SSR, Prerender, CSR and other first-screen optimization programs in the industry at the present stage. Almost all of these programs focus on putting the rendering process before traditional SPA client rendering. However, traditional performance optimization methods have little success in SPA projects due to the fatal defects of SPA itself:
SPA package
In terms of the performance of the first screen of the SPA project, we have been paying attention and exploring for a long time. During this period, we have tried many schemes, including:
-
From the perspective of reducing code size: Webpack optimization, packaging optimization, tree-shaking, etc
-
From the Angle of reducing HTTP requests: interface merge, load on demand, delay loading and other methods to reduce requests
-
From the cache point of view: offline package, HTTP & browser various cache use, DNS pre-resolution, DLL scheme, interface cache scheme and so on
-
From the perspective of data acquisition timing: webWorker prefetch data, routing entry process read data, etc
-
Start by reducing the size and number of images: use WebP images, request domain name parallel optimization, CSS Sprite, etc
These schemes can reduce the white screen time and the first screen time to a certain extent, but the effect is limited, and it is difficult to significantly reduce the data like SSR scheme. The reasons are as follows:
As you can see from the figure above, the white screen process is almost inevitable, because no matter how much you optimize the code size, the Vue family of libraries and the other core library files you need add up to at least a few hundred kilobytes. Add in the execution time of these files (measured at least 500ms), and probably most of the time, We have been blank screen for at least 1200ms-1500ms.
Of course, we can put the CSS required for the skeleton screen into HTML to display the skeleton screen as soon as possible (but many of the lower kernels will render the page only after the script is downloaded and executed), but this is not really the first screen, and even in terms of performance statistics, there is no direct feedback on the first screen improvement.
So the SSR program became our lifeline:
SSR solution
Let’s look at how SSR solves this problem:
The advantage of SSR scheme is that the HTML downloaded by the browser already has the DOM structure and style required for the first screen rendering, and the white screen time is almost equal to the HTML file download time, which is less than SPA, and the performance data has been significantly improved.
So why don’t we just go with SSR?
There are four main reasons:
1, SSR project transformation cost is high
There are two mainstream SSR schemes of Vue technology stack: official scheme and Nuxt.js. The similarities of these two schemes are as follows:
-
The existing WebPack configuration must be replaced with the above two solution projects
-
All pages of the project must be implemented in SSR mode
-
Data must be fetched within the custom asyncData/preFetch lifecycle
-
Interface data must be managed using Vuex
Maybe you think this is not difficult ah, for a new project, it is not difficult, but for an old project, the transformation cost and testing cost is extremely high, this is the reason why few old project transformation SSR.
2. SSR performance depends on interface performance
From the SSR principle, you can know that the SSR server rendering process depends on obtaining all the data before starting rendering. Once the interface delays or times out, the performance of the first screen will also be affected.
3. SSR load capacity and capacity expansion may become bottlenecks
It is almost universally acknowledged in the industry that the load capacity of Node is worse than Java, and worse than nginx static resource services. Moreover, many companies do not have too many practices and mechanisms to guarantee the rapid expansion of Node server. Although they can prepare enough servers to resist traffic peak, this is the corresponding cost after all.
4. SSR has no downgrade scheme
Once node service failure, the page may be directly white screen, most of the time not restart the service can solve, after all, SSR is not like SPA in the browser to see what error to solve or rollback can be, you must really solve the fault to restore the service, this period can not be easily degraded to SPA solution.
Among the above reasons, the most important reason that hinders us from using SSR is the transformation cost.
Prerender scheme
Prerender is implemented based on the prerender-SPa-plugin webpack plugin as follows:
The core principle is that during the webpack packaging process, Puppeteer accesses the corresponding route, captures the HTML and statically deploits the CDN.
However, this scheme is rarely used in the industry for the following reasons:
-
The static process occurs during construction, and the data that users see when they access it is destined to be obsolete.
-
This solution relies on routing using history, which is not cheap to retrofit and test for older projects.
-
The compilation time increases dramatically, just think about it.
Through the above analysis, we can see that the “optimal solution” should be SSR. If the load capacity is not considered, only the transformation cost hinders us. Can we achieve the same effect as SSR with a lower cost?
Offline prerender OPR
With a thunderbolt, OPR was created, and we named it Offline Prerender.
OPR rendering process:
OPR is a rendering service independent of the user access process, which is different from SSR rendering in the user access stage. It regularly renders the pages through Puppeteer and uploads the CDN. The pages accessed by users will be pure static pages, which can be said to combine SSR and Prerender.
Differences with SSR scheme:
-
The rendering process is independent of user access, no server pressure, minimal occupation of resources, a server can be completed
-
The page hardly needs any changes
-
The rendered page looks almost identical to SSR
-
Can be downgraded to a SPA program
Differences from Prerender schemes:
-
Timed rendering solves the problem that Prerender data cannot be updated in a timely manner
-
The page hardly needs any changes
-
There is no impact on the original project architecture process
OPR scheme implementation process
Let’s simply disassemble:
1 Periodically access the page
We first set up a Node service that periodically accesses the rendered pages through Puppeteer through the schedule mechanism.
2 Wait for the page to render
Page rendering is a dynamic process, and how do we know when a page has been rendered? Puppeteer offers several solutions, but the solution we have chosen is to monitor the timing of the company’s performance statistics, which can be easily implemented using Puppeteer’s Page.waitForRequest method.
3 grab HTML
You must be aware that we are fetching the HTML rendered by the browser, not the contents of the index.html file you requested.
OuterHTML is what you can think of as using the browser developer tools. Select the HTML tag and right click to copy outerHTML.
For the latter, you can look at the HTML source code in a browser, which should only have a blank DOM and some <script> tags.
The former might look something like this:
The latter goes something like this
$eval(‘ HTML ‘, e => e.uterhtml); $eval(‘ HTML ‘, e => e.uterhtml);
OPR render identifier
To let the page know that it was rendered by OPR, we will inject a variable __offline_PRERender_data__ into the HTML, which is used both for identification and for storing special data
Fetching interface data
Those familiar with THE SSR process may know that SSR will write the data required by the server rendering stage into HTML, and the client will perform a data verification during rendering. With these data, the client can also complete the second rendering as soon as possible (as described below).
OPR also provides the ability to:
If the developer has set useDataCache in the configuration file: True, we will listen for all interface requests until the page is rendered, and input the data into THE HTML. We will also help you to inject a piece of code that will enable the HTML execution to store the data to localStorage for use by some interface cache
The general code is as follows:
4 Solve adaptation problems
The device environment we simulate in the server puppeteer is definitely not the same as the user’s actual device, so we need to solve the style adaptation problem:
A, inject rem refresh code into head
B. Convert the px on the page to REM
The solution is relatively simple, so I won’t post the code here.
5. Remove useless content
We’ve stripped HTML of useless content, including:
6 contrast
We expect to reduce the frequency of updating the CDN, so we will compare the rendered HTML with the HTML rendered last time. If the content is consistent, we will not repeat the rendering. There are two main situations that lead to inconsistent rendering:
A: A new page was launched during the process
In this case, the <script> address in the two HTML is not the same, and must be online again
B: The data on the page has changed
-
Need to go online: The interface data changes
-
No need to go live: Changes in unimportant data on the page, such as countdown, number of purchases, etc
To reduce the number of prerender-tag-nodiff cases, add an offline-prerender-tag-nodiff style name to the DOM so that OPR does not compare to the DOM in diff.
7 upload CDN
OPR will upload the rendered page to the CDN according to the URL, for example:
Around book home page: https://m.zhuanzhuan.com/open/ZZBook/index.html#/Book/Home
Will be uploaded to the cdn:https://m.zhuanzhuan.com/open/ZZBook/index-Book-Home.html#/Book/Home
Why not index.html? This has several advantages:
A: Distinguish the OPR address from the SPA address
We just need to replace the entry address with
https://m.zhuanzhuan.com/open/ZZBook/index-Book-Home.html#/Book/Home
The user will be able to access the OPR rendered page. If the user accesses the original index.html, they will only be accessing the original SPA page, but the rendering will be slower.
B: Reduce risk
Imagine if we wrote the file as index.html. If the user first visited the /Book/Mine route, the user would wonder why you showed me the first page when I visited the personal-centric page.
It also protects against the risk that the OPR service will have an accident and all pages will become inaccessible.
The second render
As mentioned above, both SSR rendered and OPR rendered pages will be rendered twice by the user:
-
The browser renders the DOM in HTML (we call it first render, note that the page is static and not interactive)
-
Vue also re-renders and maintains a virtual DOM and remixes the virtual DOM with the HTML DOM (since the two DOM are identical, you can’t see the changes at all, but you can interact with them, which we call “secondary rendering”).
There has to be a second rendering. Why?
Because the first rendering is just a DOM level presentation, we have to put the whole Vue logic into the DOM, otherwise there will be no events and users will not be able to interact.
Both SSR and OPR methods have the same first rendering process, but there is a big difference in the second rendering:
SSR:
The Vue code creates a special process for SSR: it tries to mix with the DOM in the page, and if it is exactly the same, it reuses the DOM directly and mixes the virtual DOM into the page:
OPR:
The secondary rendering of OPR projects is not as easy as expected, and it is difficult to reuse DOM, mainly because:
OPR pages are difficult to achieve DOM consistency
Because SSR project is to get all the data for one-time rendering, no matter the time when the server generates THE HTML or the client renders the second time, the data is also the same, so it can render the DOM exactly the same. If there is the slightest difference between the virtual DOM and the DOM in the page, Vue removes the existing DOM from the page and uses the new DOM.
SPA pages have a gradual use and rendering process, such as:
Rendering in OPR or SPA pages is done multiple times:
1. Finish rendering the first time when ready=true and start rendering the goodsList component
2. GoodsList triggers a second render
The first time the DOM is rendered it doesn’t match the page, so the old DOM is discarded.
That if the page we can write more a little bit more simple of structure, such as all components are out, don’t v – if control, all data use the cached data, and cannot contain asynchronous read data synchronization process, this theory can fully reuse the DOM, but in reality is not possible, the business logic of responsible than imagined, We certainly don’t want a lot of reengineering of the business, which defeats the purpose of OPR.
Therefore, the secondary rendering solution chosen by OPR scheme is: delay mount DOM
The core of this solution is that as long as you do not pass the EL parameter during the new Vue process, the Vue will complete the rendering and construction of the virtual DOM in the DOM that is not mounted to the page, so we just need to attach the DOM to the page at an appropriate time. What is the appropriate time?
Page DOM and virtual DOM are nearly identical
Let’s start with the process of “mounting” : simply delete the HTML DOM and insert the virtual DOM handle window.vm.$el into the body.
You might be worried about the performance penalty, but this DOM deletion and insertion process is actually very fast and consumes very little browser process resources.
Going back to mount timing, what happens if the pages are too different to mount? The user will feel the page flicker because some parts look different, which should be easier to understand, so we want to mount the two DOM’s when they are close to the same
Delay the mount time
In order to know when the two DOM are close to the same, we refer to the calculation scheme of the end of the first screen in taobao’s performance statistics scheme:
We use MutationObserver to listen for changes in the virtual DOM, calculate the DOM score once each change, and mount the DOM when the DOM score is close to the existing DOM score on the page
The logic is a little more complicated:
When the two DOM’s are almost identical, mount the DOM and the user will barely notice the page flicker.
As mentioned above, if interface caching is used, the time for DOM convergence is reduced.
Other optimization
In order to further improve the speed of the first screen, we also made some optimizations in the OPR scheme:
1 CSS tree-shaking
The HTML we grab will have a lot of style tags in it. You can almost assume that there are at least as many style tags in the head as there are in the vue. For example:
There are a lot of duplicate styles in these tags. If you put them in HTML, it will increase the size of your file. We tree-shaking this.
2 Change script to async loading mode
Before you do that, you might want to know the difference between normal Script, defer, and Async.
In short,
-
Plain script: download and execute in written order, block DOM rendering, block DOMContentLoaded time
-
Defer script: download in parallel and execute in written order without blocking DOM rendering and DOMContentLoaded time
-
Async Script: parallel downloading, which does not block DOM rendering, does not block DOMContentLoaded time
Why do we use async? In a lower version of iOS kernel, the page will wait for DOMContentLoaded before rendering. If we use normal Script or defer mode, the page rendering time will be delayed.
3 Degradation Strategy
If there are any problems, to ensure user access, we will do a downgrading scheme, which is also very simple:
Grab the text content of HTML itself and synchronize it to CDN, which is equivalent to returning OPR mode to SPA. This strategy will have corresponding detection mode and automatic switching mechanism in OPR scheme.
The effect
OPR scheme has been used in many businesses of our company, and it can reduce the SPA page from the original first screen of 1500ms-4000ms to 300ms-800ms, which can be said to have remarkable effects
The last show
OPR scheme is currently applied to render the home page and important list of our businesses without login state. In the next stage, we will solve the page rendering dependent on login state. Readers are welcome to discuss with us.
Author’s brief introduction
What zhang yong
Around the platform operations center front head, with deep research in the area of the front end, including: the sketch a key figure, front-end data modeling, multiple aspects such as the small program foundation ability construction, 10 years working experience in engineer for 2 years and 5 years CEO, technology management, 3 years to write the article, is the nuggets 2018 outstanding authors.
Scan the QR code to follow us