skip to content
usubeni fantasy logo Usubeni Fantasy

Vue Project Optimization

/ 7 min read

This Post is Available In: CN EN

Below, I will share several optimization techniques in order of effectiveness. At the bottom, there will be a quick summary by category.

Component Splitting

I used to think that splitting sub-components was for abstraction, but practice has shown me that splitting sub-components is a way to improve performance (in specific cases).

In my actual work, I encountered a problem where there was a large table with multiple dialog boxes for adding new entries. When there was a lot of data, it became slow to fill in the new data in the dialog box.

The reason is that within a single component, modifying a value triggers the render function and diff for the entire component. But I know that I’m just filling in a number in the dialog box, and the form itself hasn’t changed. Why should I waste time checking unnecessary things?

To solve this problem, extracting the dialog box as a separate component became a very effective optimization method.

Because of Vue’s update mechanism, updates are done on a per-component basis. Updating a child component does not trigger an update in the parent component, unless the child component changes the parent component’s data.

Let’s take the example of the dialog box in element UI (open this link to directly run the example).

Create a page that contains two dialog boxes, one directly written in the page and the other abstracted as a component.

First, let’s look at the componentized dialog box, which is very simple:

<template>
<el-dialog :append-to-body="true" title="提示" :visible.sync="visible" width="30%">
<el-input v-model="xxx" />
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取 消</el-button>
<el-button type="primary" @click="visible = false">确 定</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
props: ["dialogData"],
data() {
return {
xxx: "",
visible: false,
};
},
};
</script>

The main page is as follows, containing an el-dialog and an already abstracted my-dialog:

<template>
<div>
<div>{{ renderAfter() }}</div>
<el-button type="text" @click="dialogVisible = true">Click to open Dialog</el-button>
<el-button type="text" @click="showMyDialog">Click to open Dialog2</el-button>
<el-dialog :append-to-body="true" title="Prompt" :visible.sync="dialogVisible" width="30%">
<el-input v-model="xxx" />
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible = false">Confirm</el-button>
</span>
</el-dialog>
<my-dialog ref="myDialog" />
</div>
</template>
<script>
import MyDialog from "./Dialog";
export default {
components: { MyDialog },
name: "HelloWorld",
props: {
msg: String,
},
data() {
return {
xxx: "",
dialogVisible: false,
};
},
methods: {
renderAfter() {
if (!window.renderCountTech) window.renderCountTech = 1;
else window.renderCountTech++;
console.log("%cRender function check", "color:#fff;background:red", window.renderCountTech);
},
showMyDialog() {
this.$refs.myDialog.visible = true;
},
},
};
</script>

In practice, it is known that when opening or closing the el-dialog and modifying the data in the input box, the render function of the entire component will be triggered, while the my-dialog will not trigger the update of the parent component regardless of whether it is opened, closed, or input. When the data in the component containing the dialog is small, there is indeed little difference, but when the data is large, there will be noticeable lag when inputting in the dialog box. (In short, the dialog box is a separate component, and internal updates do not affect the parent component)

Moreover, conversely, when the parent component is updated, the el-dialog will be rendered without rendering the my-dialog, which is truly a win-win situation. (In short, the child component without data changes will not be changed when the parent component is updated)

Even if this component is not reused, you can separate the methods used in the dialog box into a separate file, without mixing them with the methods of the main page. If a dialog box has a lot of logic, separating it into a separate file is definitely a good approach.

However, there are also disadvantages: data interaction is a bit inconvenient, but you can try to use $refs, $parent, Vuex, and other methods to solve it.

If you don’t like the solution I used above, this.$refs.myDialog.visible = true;, you can also use the traditional $emit and $on methods.

For a more detailed explanation of the update granularity, you can refer to Vue and React: What are the differences in component update granularity.

Additionally, you can learn about optimizing child component updates in React in React Rendering Optimization: diff and shouldComponentUpdate.

P.S. There is also a hidden conclusion that if a component uses slots, the child components will be re-rendered along with the parent component.

Replacing Heavy Components

Open source libraries benefit everyone, but the more people they serve, the more redundant logic they contain. Normally, they do meet performance requirements, but if you really encounter a situation in your business where you need to loop through dozens or even hundreds of tables using v-for, the efficiency impact caused by redundant logic should not be underestimated, and it will also consume more memory.

So, try creating your own libraries in the simplest way possible to meet your minimum requirements. Personally, I created a table component cpn-tbl and a lightweight validation plugin v-vld because I had too many tables and the table component in element UI was too complex.

When implementing components, you may need to be more familiar with the usage of the render function, so that you can manipulate the node structure more freely and concisely.

v-if

If you need the initial rendering to be as fast as possible, v-if is definitely your good helper.

In addition, using asynchronous components in v-if can delay the loading of some code.

new Vue({
// ...
components: {
"my-component": () => import("./my-async-component"),
},
});

With the help of webpack’s code splitting feature, you can easily achieve such asynchronous components. Of course, code splitting can also be used to delay the loading of large JavaScript files (such as heavy libraries).

P.S. Speaking of code splitting, this is an optimization method that must be understood regardless of which framework you use, especially with Webpack, where you can use simple syntax to split code and pursue the ultimate speed of the initial rendering.

On the other hand, using v-show can cache components and increase the smoothness of toggling.

Chunk Rendering

This is an optimization particularly aimed at improving scrolling smoothness. As we all know, browser rendering and JavaScript execution are mutually exclusive. If a piece of code takes more than 1000/30 ms to run while the browser needs to render, it will appear to be less than 30 frames.

Related link: requestAnimationFrame

Caching AJAX Data

You can encapsulate it to be used just like a regular axios GET request, directly replacing the original axios object:

import axios from "axios";
import router from "./router";
import { Message } from "element-ui";
let baseURL = process.env.VUE_APP_BASEURL;
let ajax = axios.create({
baseURL,
withCredentials: true,
});
let ajaxCache = {};
ajaxCache.get = (...params) => {
let url = params[0];
let option = params[1];
let id = baseURL + url + (option ? JSON.stringify(option.params) : "");
if (sessionStorage[id]) {
return Promise.resolve(JSON.parse(sessionStorage[id]));
}
return ajax.get(...params);
};
ajax.interceptors.response.use(
function (response) {
// Other processing
// ...
if (response.data.code === "20000") {
let params = response.config.params;
let id = response.config.url + (params ? JSON.stringify(params) : "");
sessionStorage[id] = JSON.stringify(response.data.data);
return response.data.data;
}
},
function (error) {
Message.error("Connection timeout");
return Promise.reject(error);
},
);
export default ajaxCache;

If you think the response.config.url + (params ? JSON.stringify(params) : '') id is too long, you can add a hash() to shorten it, but it will lose readability.

Freezing

For read-only data such as tables, there is no need to make the data “reactive”. Object.freeze() is used for this scenario to prevent assignment to reactive data, saving the time of recursive “reactivity”. For large and deep objects, the time saved is quite significant.

Key for Loops

Purpose: to speed up the diff algorithm. However, remember not to use a simple index as the key. Also, when looping through a template, each child element needs a different key.

Speaking of which, I suddenly remembered that when using Element UI to render el-form-item in a loop, I wrote the key incorrectly, which caused infinite re-rendering. I debugged it for a long time 😂

Categorization Overview

Component-level optimization

  • Split components to optimize update speed at the component level
  • Be cautious with heavy components; create your own when necessary, the same applies to plugins
  • Use functional components (lower priority)

Handling side effects of reactivity

  • Utilize reactive anti-patterns
  • Reduce the use of this in dependency collection (can optimize initial rendering speed)

Reducing rendering pressure

  • Balance between v-show and v-if
  • Render in chunks

Vue’s built-in caching

  • keep-alive
  • computed

Other optimizations

  • Data caching
  • Virtual scrolling
  • Remove console.log
评论组件加载中……