Qi Yunlei is the front end engineer of Wemedical Cloud Service Team. This article is a text version of the topic shared by the author in the second Colorful Front End Technology Salon.
Most readers will be more interested in the performance optimization in the title, but the focus is more on practice. There are deep and shallow practices, the following will be introduced when there is a large emphasis. Of course, length does not mean difficulty, and given that a lot of the information is already out there in the public domain, I will only mention a few key words in this section, hoping to serve as a primer.
This share will focus on the background of Vue SSR and related business growth, and show you what we have tried, as well as some pit stepping experience, hoping to bring some reference value to small and medium-sized teams.
For large teams, this is where basic optimizations may be commonplace. And many people in order to squeeze the performance of the machine, the pursuit of extreme, there have been a variety of successful exploration. We learned a lot about ideas, but no matter how good they are, they have their limitations, and what works for them may not work for us.
Limited by the experience and level of sharing people, this article is mostly from the perspective of Server to think about how to solve the problem, there are unavoidable omisses, hope readers greatly criticize and correct.
1. Practical background
Practice and context are closely related, so before we go any further, let’s explain the context of our performance optimization.
First of all, I have to mention the industry background. By the way, I would like to post an introduction to the company: Wedoctor is an Internet medical enterprise. It has been fighting in the front line of digital health field for more than ten years, and provides one-stop medical and health care services integrating online and offline for the vast number of users. Before this year, the peak traffic of the healthcare industry was much smaller than that of other services, especially the real core business of healthcare, medicine, medical inspection, etc., so it was unlikely that there would be high concurrency.
At this point, it’s probably clear to everyone that 2020 marks an important turning point — COVID-19.
Business background
The first problem is that traffic is going up, and more importantly, not knowing how much traffic is going to come in. Of course we can’t expand the machine indefinitely.
The second problem, really began to face the users of the national region. Teams using cloud servers can also add nodes in different regions, but Wedoctor uses its own computer room for most of its business, even near Hangzhou. Users in other areas are too far away, the network experience is poor, and access speed is slow.
Technical background
For those of you who are familiar with SSR techniques, I’ll just mention the pros and cons of server-side rendering for those who are not.
SSR advantage
As the server directly out of the page, thus shortening the content arrival time, reduce the home page white screen.
Straight out of the page contains key data page information, search engine crawler more friendly, conducive to improve the site search ranking. Because mainstream crawlers don’t parse JS scripts, some SEO-focused applications have to use SSR.
By the way, SSR rendering now generally refers to isomorphic rendering, which can take into account most of the advantages of client-side rendering.
SSR defects
The shortcomings of SSR are also very prominent, the first problem of nature is that the server pressure is larger than the client, which is in line with the rule of demolishes east and west. SSR improves the client-side first-screen experience by squeezing the performance of the server, and rendering the page is a computation-intensive task that is not efficient enough for node.js written services. With complex page components, a small amount of concurrency can wear down a process.
Another potential problem is the impact on the development experience. A front-end team with no backend experience may not have enough control over the service layer code, and using SSR is risky. However, because we decoupled the SSR server from the client, the development experience is not that different from CSR.
Second, program discussion
Once you have the problem, let’s break it down. Here, borrowing a map, the life cycle of an SSR request is divided into three phases, mainly pulling out the part that performs the rendering from the whole
FCP: first content drawing time, TTI: interactionable time
It’s worth explaining, however, that the usual disassembly is between the request phase initiated by the user from the browser, the server rendering phase, and the response phase, but in this way the battle lines are stretched and the scope for optimization is too large. But our core appeal is to relieve the pressure on the server, not to blindly pursue the limit number.
Therefore, we have specifically narrowed our view and only understood the three stages from the server’s point of view
- The request has reached the service and has not rendered
- Start rendering calculations until rendering is complete
- The server processes the response
The fundamental performance problem with SSR is in the middle, CPU intensive.
So Vue3 brings a change. Conservative can improve rendering performance by 2 to 3 times. But is there any way we can improve the performance of this step before Vue3 arrives?
One of the main reasons for optimizing Vue3 is to change as much of VDOM rendering as possible to string concatenation, and we can do the same for Vue2. However, Vue’s entire rendering process allows us to intervene in very little, let alone involve the replacement of the underlying algorithm. How do we do that?
Before Vue3, many of its predecessors had already done so. For example, in last year’s Tweb sharing, a lecturer shared a scheme to optimize SSR performance to the extreme, which used a self-developed compiler to replace vue-Loader and generate linear string splices according to vUE syntax tree during compilation, and then no longer needed to construct and iterate VDOM.
However, the general consequence of this is that Vue’s entire syntax is not compatible, and even Vuex is no longer available. Unfortunately, the logic of our page is very complex, and we also rely heavily on Vuex management state. If the project is substantially reformed in order to try such a scheme, the cost performance is too low. What’s more, with Vue3 in the future, let’s put this tricky issue on the back burner and focus on the other two optimizable phases.
Three, conventional optimization
Performance optimization is always going on, of course, and there are some common methods already in use, so let’s take a look at a few by rendering stage.
Before rendering
In response to that, from the server’s perspective, there are a few things you can do to lighten the load before the rendering task even begins.
First, multi-level caching. Interface data, components, and pages spit out can all be cached. The core of this step is to continue to shift CPU pressure to memory, the former to shorten the request link, and the latter two to reduce rendering computations. The way of caching is very flexible, simple a little bit directly with memory cache, with LRU algorithm is basically enough. Complex scenarios require in-memory databases such as Redis.
Second, request reuse. The most notable option is to use http-agent with keep-alive enabled. This allows subsequent requests to reuse the connection, reducing the number of repeated handshakes.
Third, downgrade the circuit breaker. If there is no degradation, although node.js nodes are stable and will not break down due to pressure, there will be a backlog of requests, resulting in the timeout of the back-end interface of Node.js requests and the service will be unavailable.
Looking back, what’s the problem with that?
For our team, most components rely on global state and there are few scenarios where component caching can be used, so we mainly use page caching. If the service has highly customized pages, there is a cache that cannot be reused among different users, which may consume a large amount of memory. Memory is also a valuable resource for servers, but its misuse poses a greater risk than its cost and performance. Caching is a very complex subject, and its side effects will be covered in a later section. In short, you have to be prepared to avoid caching.
Let’s talk about demotion. On the one hand, degradation will release the stress on the SSR service to the client, and the browser cannot read the interface data cached by the SSR service layer when rendering the page, and instead directly requests the back-end service. This is a protection for SSR processes, but not a good thing for back-end applications. On the other hand, if you degrade only when an exception occurs, and the requests pile up and time out, the degradation does not relieve the stress and the overall page response time is delayed. Therefore, demotion strategy also needs to be flexible and perfect implementation.
After the rendering
After the page is rendered, we do a series of experience optimizations, and these two are the main performance optimizations.
CDN can be simply understood as a set of proxy servers. The so-called CDN speeds up static resources because resources are cached to proxy servers. The content of static resources generally does not change frequently and is therefore better suited for caching than dynamic page data.
It’s important to note that gZIP compression can be done in several ways. Recently, CDN has occurred the problem of removing gZIP response header, resulting in compression did not take effect, the content size difference of more than ten KB, but the page response time difference of 400ms.
Iv. In-depth practice
These are some of the optimizations that have been made before business growth, but the real pressure is still to come.
Basic network tuning
Intranet call
This is a fundamental issue that was overlooked early on.
Initially, our SSR server accesses the back-end interface through the gateway domain name of the public network, but the efficiency of resolving the domain name from the public network is extremely low. Although keep-alive can be used to reuse connections to some extent, there is still a process of periodically establishing connections, which makes for a poor network experience.
In order to steadily shorten the interface invocation time, we changed the domain name resolution of the public network to configure host to directly access the gateway IP address, but limited to the gateway configuration, the HTTPS protocol is still used. After negotiation with O&M, the Intranet domain name invocation in HTTP mode was changed.
Here’s a little bit of an extension. Before o&M, using IP addresses to access gateways is risky. If there is only one IP address, single point of failure is likely to occur. If there are multiple IP addresses, load balancing and DISASTER recovery are required.
Load balancing is primarily to avoid congestion, which requires that we should record multiple gateway IP addresses and poll access to ensure that traffic is evenly distributed to multiple gateway servers.
Dr Requires that when a node fails, we can automatically remove the failed node and add alternatives after it recovers.
In addition to the above basic situation, there is actually the problem of traffic distribution weight. Imagine that different servers have different performance, network bandwidth, and so on. What do we do when we want the best to work harder?
If you don’t have experience dealing with this situation, Nginx’s weighted smooth polling is recommended, which is its default load-balancing algorithm.
Weighting and polling are easy to understand. What is smoothing? For a node with high weight, the traffic passing through it will not fluctuate. The more stable the frequency is, the smoother the load balancing algorithm is.
Because the implementation of the algorithm is very simple, uninformed students can find information by themselves. The above description is still a very basic model, suitable for the transition of the network environment, ultimately let the gateway and o&M provide support as well.
Extended multilevel caching
For high-concurrency scenarios, we all know the importance of caching pages, but how do we do it?
With different rendering schemes, there are mainly two directions. One is based on CSR, which can deploy all pages to CDN and enable CDN cache. The other is SSR as the main body, mostly rely on their own cache middleware hard resistance, rely on huge Redis and MQ cluster, to design the traditional back-end server ideas to deal with.
Prior to this, Wedoctor’s rendering service was relatively simple, with almost only a memory cache, resulting in an exaggerated memory footprint for the Node.js process. Now faced with the choice between CDN caching and the introduction of Redis cluster, in fact, it is not a choice, both optimization is worth doing, we take the most moderate CDN cache for the current architecture first.
This section describes CDN cache
A CDN was introduced earlier when we talked about static resource caching, but it does more than just cache static resources. This section shows how we can place dynamic pages rendered by SSR on the CDN cache, which has many different concerns than static resources.
Here’s a series of questions and answers to take you closer to the topic.
Why is the CDN connected
Abstract a simple request link, convenient to understand the LOCATION of the CDN. It looks like adding another layer of transmission costs, but it’s not that simple.
CDN utilizes its vast server resources to dynamically optimize access routes, provide nearby access nodes, and obtain data from source sites with lower latency and higher bandwidth, thus optimizing user experience at the network level.
Due to the cost, most companies do not build their own CDN cluster, but use the CDN service provided by large factories.
Let’s zoom in on the CDN node to see what it does
Under the premise of no cache, there is some loss on the link, and the overall effect still needs to be analyzed in detail, which may not bring positive optimization. But once caching was introduced, there was a qualitative change
Why is CDN caching enabled
A CDN can cache resources requested by a user and can contain HTTP response headers. The next time any user requests the same resource, it responds directly to the user with the cached resource, saving all subsequent steps that should be handled by the source site.
In short, the request link is truncated.
How do I enable CDN cache
Without considering the self-developed CDN, the steps to enable CDN caching are very simple:
- Access the CDN service by domain name and enable cache for paths
- Setting the cache-Control response header at the source site is not necessary for more flexibility in controlling caching rules
Generally, the two are not indispensable, and the rule of cache time depends on the CDN service provider.
Which services can enable CDN caching
Most websites are suitable for accessing CDN, but CDN caching can be enabled only when SSR pages meet certain conditions. After caching is enabled, all users under the same URL access the same resource. And the page data should be less time-sensitive, at least to the point of a minute delay.
CDN cache optimization
One of the most important metrics to measure cache effectiveness is cache hit ratio. Before setting up a CDN cache, let’s look at a few more points to improve cache hit ratio. These points are also appropriate criteria for evaluating whether a system should access the CDN cache.
(1) Cache time
Increasing the time of cache-control is the most effective measure; the longer the Cache lasts, the less chance the Cache will fail. It can significantly improve your cache hit ratio even when page views are low.
Note that cache-control can only tell the CDN the time limit of the Cache, and does not affect its early elimination by the CDN. Resources with low traffic will be cleared quickly. CDN protects its resources from waste by using the hierarchical precipitation caching mechanism.
(2) Ignore the URL parameter
The full URL that the user accesses may contain various parameters, and the CDN will treat them as different resources by default, each of which is cached independently.
And some parameters are obviously not expected, for example, after the page link is shared in wechat and other channels, the end is hung up with the statistical parameters set by various channels themselves. The average number of visits to a single resource will be greatly reduced, thus reducing the caching effect.
In some CDN background Settings, filter parameters can be enabled to ignore urls. The following parameters. In this case, the same URL is always the same resource file.
(3) Active cache
It is possible to achieve 100% cache hit ratio by turning passive into active. The commonly used active cache is resource warm-up, which is more suitable for static files with clear URL paths. Dynamic routes cannot be given to CDN intelligent warm-up, unless specific addresses are pushed in turn.
Application code evolution
After talking about several points of CDN cache optimization, it is clear that CDN background configuration needs to be treated with care. In practice, I also went through several stages of adjustment, but after all, the specific configuration depends on the CDN service provider, so THIS article will not go into depth.
Now it’s time to turn our attention to the evolution of the code layer.
1. Control the cache
Code configuration has a premise, that is, the CDN background needs to enable the source Cache-Control support.
Then, by simply adding the response header, you can take over from operation and maintenance the initiative to set CDN caching rules.
Taking Node.js Koa middleware as an example, the global initialization version is as follows
app.use((ctx, next) = > {
ctx.set('Cache-Control'.`max-age=300`)})Copy the code
Of course, the omissions in the above code are numerous. In SSR applications, there is less need to cache all the pages, which is to supplement the path criteria.
2. Control the path
Although the CDN background can also configure the path, the configuration mode and even the number of paths are limited, which is less flexible than the code form.
If we only need to cache the /foo page, we will add the if judgment
app.use((ctx, next) = > {
if (ctx.path === '/foo') {
ctx.set('Cache-Control'.`max-age=300`)}})Copy the code
This leads to the first pitfall, and it’s important to pay attention to how routing handles path. In general, ‘/foo’ and ‘/foo/’ are two separate paths. Ctx. path === ‘/foo’ may have missed the processing of requests with path /foo/.
3. Add paths
The pseudocode is as follows
app.use((ctx, next) = > {
if ([ '/foo'.'/foo/' ].includes(ctx.path)) {
ctx.set('Cache-Control'.`max-age=300`)}})Copy the code
In addition, the CDN background configuration also needs to avoid this problem. In Tencent CDN, directories and files are applicable to different page paths.
4. Ignore the degraded page
In the event of a server rendering failure, to improve fault tolerance, we will return the degraded page and convert it to client rendering. If the CDN caches degraded pages due to accidental network fluctuations, the user experience will continue to be affected for a period of time.
So we’re introducing the ctx._degrade variable again, which indicates whether a page has been degraded
app.use(async (ctx, next) => {
if ([ '/foo'.'/foo/' ].includes(ctx.path)) {
ctx.set('Cache-Control'.`max-age=300`)}await next()
// When the page is degraded, uncache
if (ctx._degrade) {
ctx.set('Cache-Control'.'no-cache')}})Copy the code
Yeah, that’s not the last trap.
5. Cookie and state governance
As mentioned above, the CDN can optionally cache HTTP response headers, but this option applies to the entire domain name and is generally required.
The new problem comes from a response header that doesn’t want to be cached.
The setting of the application Cookie depends on the response header set-cookie field. The cache of set-cookie directly causes the Cookie of all users to be refreshed to the same.
There are several solutions. One is not to Set any cookies on the page, and the other is to filter out the set-cookie field at the proxy layer. Unfortunately, Tencent CDN currently does not support the response header filtering, this step of fault tolerance must be operated by oneself.
app.use(async (ctx, next) => {
const enableCache = [ '/foo'.'/foo/' ].includes(ctx.path)
if (enableCache) {
ctx.set('Cache-Control'.`max-age=300`)}await next()
// When the page is degraded, uncache
if (ctx._degrade) {
ctx.set('Cache-Control'.'no-cache')}// Cache pages do not Set set-cookie
else if (enableCache) {
ctx.res.removeHeader('Set-Cookie')}})Copy the code
The code added above is designed to remove set-cookies before the page responds, but the middleware loading order is difficult to control. Some plug-ins, in particular, implicitly create cookies, which makes clearing cookies extremely cumbersome. If subsequent maintenance personnel are unaware, set-cookies are likely to be readded to the response header. So try to do this at the proxy level, not in code logic.
In addition to cookies, you may face other state information management issues. For example, the login state of the requested user is stored in the renderState of Vuex. At this time, user information is embedded in the HTML page. If cached by CDN, problems similar to set-cookie that are not cleared will occur on the client. There are many similar examples, and their solutions are very similar. Before accessing CDN cache, make sure to do a comprehensive check on the status information.
6. Customize the cache path
Now that the functionality is working properly, however, the caching rules are so complex that if you want to set up more pages, you need to customize the cache time separately. This code still needs to be changed constantly.
For example, we only want to cache /foo/:id, not /foo/foo, /foo/bar, etc.
Note that the CDN background may only support a Cache path starting with /foo/, which requires that we add ctx.set(‘ cache-control ‘, ‘no-cache’) as the default in the first line of the middleware.
For example, if we want to cache the /foo page for 5 minutes and the /bar page for 1 day, we need to introduce a time profile.
The middleware and corresponding configurations become more and more difficult to maintain.
Therefore, in another way, the cache rules are no longer handed over to the middleware, but transferred to the entry-server of Vue SSR. The page level configuration can be achieved through metadata. Due to the differences of SSR schemes, the specific implementation is not repeated.
7. The cache is invalid
Cache invalidation is a neutral word. The pros and cons of CDN cache invalidation have to be weighed carefully.
On the one hand, it intermittently increases service stress and, in Serverless applications, computing costs. On the other hand, in many scenarios we had to trigger it to really update the resource.
The dark side of CDN caching is hard to ignore. Caching is transparent to the user, but can be a hindering factor to the product and technology.
If not handled properly, it will affect the timely release of new functions, block the burial sites of all services, increase the cost of risk perception, and fail to guarantee consistency, which increases the difficulty of troubleshooting online problems.
Therefore, it is important to have a trigger service that is responsible for cache refresh and warm-up to improve the developer experience. However, CDN cache controllability is very low, refresh can not be fully real-time effect.
On frequently changing pages, it is best to consider entering a stable period before opening CDN cache. Even for stable, high-traffic pages, precautions against CDN cache penetration need to be considered.
Once the CDN cache is reused in the SSR architecture, be prepared to make long-term adjustment decisions.
Page statics
Where the CDN cache cannot reach, we can also harden our own multilevel cache.
Under dynamic routing, the page path is scattered, and the traffic allocated to the specific URL of the page may not be high. Obviously such a page is not suitable for CDN cache, the cache hit ratio is very low, so the pre-rendering scheme of static page is introduced.
After the page is rendered normally, we can cache the entire page and persist the cache from memory to the hard disk or cloud storage service. In this way, a large library of pages can be fully “cached” at a low cost.
This is both a good complement to the CDN cache and can be widely used for page faces.
Five, the summary
The above optimization, within the scope of our ability to move, there are still a lot of problems and defects, I hope to provide a detailed case for friends who have never tried this kind of.
A large portion of this article is reserved for relatively rare optimizations, especially multilevel caching. The figure above is a rough performance comparison, where the biggest factor is CDN caching. At the end of this article, I will also focus on summarizing this transformation.
CDN cache is a sharp blade, in the case of large traffic, can intercept almost all requests for the source site, can provide a highly scalable load.
But is your SSR application suitable for CDN cache access?
Again, enumerate the many problems mentioned above:
- Path control
- Page down
- State governance
- Cache invalidation
The answer is up to you…
In fact, very few SSR page scenarios, such as portal home pages, require CDN caching. For general services with low traffic and scattered paths, only dynamic CDN acceleration and static file caching can be used to basically meet the optimization requirements of the CDN proxy layer.
That’s all for my share today, thank you!