preface

Performance optimization of the article I believe that students have read a lot, most of the routine is basically understood. For example, optimization for resource files (Webpack optimization, Gzip compression, CDN acceleration, browser caching, lazy loading, HTTP2, etc.), page rendering optimization (SSR, virtual list, reduced backflow redraw, etc.).

But we’re dealing more with business, and most of the performance issues are the result of developer oversight. Due to the continuous iteration of business functions, similar performance problems were found in the project recently. The following is a relatively hidden performance problem encountered in the troubleshooting process, and some ideas were obtained to share with the students in the process.

In this article you will learn:

  • Using the Performance Tool
  • Record and analyze the fire chart
  • Principle of calculating attributes, cloneDeep process

Performance issues

The project is divided into multiple modules. Each module relies on The QuestionList stored in VUEX. Based on QuestionList, the module will construct application data and render.

Next, the main performance problem is that when a configuration of an Item in QuestionList is changed, it takes ~1.5 seconds to respond. As follows:

After optimization:

It’s just silky. Here’s how to optimize the process.

The Performance analysis

Performance is a visual analysis tool provided by Google Chrome. It is used to record and analyze all the activities of an application at runtime, helping developers to locate Performance problems.

The preparatory work

In order to avoid the influence of other factors on Performance analysis, some preparations need to be made:

  1. Enable the traceless mode of the browser to prevent the impact of the Chrome plug-in on page performance
  2. Close the Vuex Logger plug-in in the development environment project, the deepCopy in the plug-in will increase the execution time (see whether the project is used, mainly reduce the impact of the project Logger plug-in).

How to use Performance

F12 Open developer tools and select The Performance panel:

There are three little buttons in the upper left corner. Click the solid circle button, and Performance will start to help record the user’s subsequent interactive operations. Click the round arrow button, Performance will reload the page, calculate the Performance during the loading process; Click the last button to clear the record.

Because we are to record the user’s actual operation process, so choose the circular button to record. First click the round button to start recording, then change the configuration on the page, and click Stop to stop recording.

Just wait a little while, and Performance will output a report.

The report is divided into three parts:

  • Overview panel: Performance charts key metrics such as page frame rate (FPS), CPU consumption, network request traffic, V8 memory usage (heap memory), and so on in chronological order.
  • Performance indicator panel: it is mainly used to show a variety of performance indicator data in a specific period of time. The most commonly used is Main, which records the task execution records of the Main thread of the rendering process.
  • Details panel: The performance panel provides only general information. If you want to view the details of these records, you can select any historical data of a performance indicator in the performance panel and then select the record details to display them in the details panel.

Analytical flame chart

Click the Main indicator to expand the task execution record of the Main process. For easier analysis, drag to select an interval in the overview panel to analyze the task execution record in detail.

The bars represent tasks to be performed one by one. The longer they are, the more time they will take. Vertical represents the execution record of the task, the main task is at the top, and the sub-tasks are further down, like the function execution stack. It is also commonly called a “flame map”.

The longest task is get task. The following subtasks are updateComponent, Render, compomutedGetter, Evaluate, etc. Before I read part of Vue source code, You basically know that it’s dependent on property changes to reevaluate the computed property, followed by page rendering. But these are not significant for our investigation, only some hints, because they are Vue internal execution of the function.

SvgHeight is a business function that needs to be checked first.

Locate bottlenecks

Find the relevant code:

computed: {
  questionList () {
    let list = cloneDeep(this.QuestionList).map((q, qi) = > {
        // ...
        return q
    })
    return list
  },
  svgHeight () {
    let h = 0
    this.questionList.forEach((q, i) = > {
      h += this._getQuestionHeight(i)
      if(i ! = =this.questionList.length - 1) {
        h += CONTAINER_GAP_HEIGHT
      }
    })
    return h + CREVICE_HEIGHT
  }
}
Copy the code

This is where the bottleneck can be roughly located, and when the configuration is changed, reactive data changes allow the calculated property (svgHeight) to be recalculated.

Bottleneck of thinking

Bold assumption

As you can see from the code above, svgHeight depends on another computed attribute (questionList), which in turn depends on questionList in VUex.

Seeing this, I have a question. The calculated attribute only relies on the responsive attribute of QuestionList, and does not depend on the specific configuration field attribute. Why does recalculation occur when the configuration is changed?

When I was puzzled, I suddenly saw that QuestionList used cloneDeep for deep copy. I had a flash of inspiration, deep copy internal is not going to traverse the properties of the copy. When iterating over a property, the get hijacking function of the reactive property is triggered, and then the dependencies of the calculated property are collected internally, resulting in excessive collection of dependencies. When the reactive property changes, updates of the dependencies are triggered.

Let’s take a look at how the cloneDeep process and calculated properties collect dependencies.

CloneDeep process

CloneDeep. Js:

function cloneDeep(value) {
  return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);
}
Copy the code

The cloneDeep method primarily calls baseClone.

_baseClone. Js:

function baseClone(value, bitmask, customizer, key, object, stack) {
  // 1. Parameter processing (assign, judge return)
  // 2. Initialize parameters
  // 3. Check the loop reference return
  // 4. Copy Set and Map

  var keysFunc = isFull
    ? (isFlat ? getAllKeysIn : getAllKeys)
    : (isFlat ? keysIn : keys);

  var props = isArr ? undefined : keysFunc(value);
  // 5
  arrayEach(props || value, function(subValue, key) {
    if (props) {
      key = subValue;
      // value[key] Accesses attributes
      subValue = value[key];
    }
    // Recursively populate clone (susceptible to call stack limits).
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
  });
  return result;
}
Copy the code

BaseClone internally iterates through access properties, recursively calling baseClone, and if it is a reactive property, its get-jacking function is triggered to collect dependencies.

Calculate property collection dependencies

/ / source location: / SRC/core/instance/state. Js
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
    
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if(! isSSR) {// create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true})}// component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if(! (keyin vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}
Copy the code

When the calculated property configuration is initialized, a “calculated property Watcher” is created in a loop for each calculated property Watcher.

/ / source location: / SRC/core/observer/watcher. Js
evaluate() {
  this.value = this.get()
  this.dirty = false
}
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm) // Evaluate attributes
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
      throw e
    }
  } finally {
    popTarget()
    this.cleanupDeps()
  }
  return value
}
Copy the code

The get hijack function that evaluates the attribute is triggered during template rendering, and evaluate is called, followed by get.

PushTarget attaches the current calculated property Watcher to the global dep. target.

Perform this getter (defined when calculating properties corresponding to the callback function), internal if access to the reactive properties, will trigger a responsive properties get hijacked function, to rely on collection. When collecting dependencies, we get them from dep. target, so we call them Watcher.

/ / source location: / SRC/core/observer/dep. Js
notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  if(process.env.NODE_ENV ! = ='production' && !config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort((a, b) = > a.id - b.id)
  }
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}
Copy the code

Each reactive property holds Watcher in an array of subs. When the set hijacking function is triggered, notify is executed and Watcher’s update method is looped through.

In combination with the actual scenario, svgHeight performs the corresponding calculated property callback during initial template rendering, and cloneDeep traverses each reactive property, which collects the corresponding “calculated property Watcher”. Later, when the reactive property changes, the calculated property updates are triggered.

For more on the Vue principle, check out my previous post:

  • Touch your hand to understand the Vue responsive principle
  • Hand touching takes you to understand the Computed principles of Vue

Caution beg a certificate

Of course, this is just a guess (I think it’s a good guess), but in the spirit of rigor, I need to do some experiments to verify my guess.

How do you test that? Since iterating over reactive properties in compute properties collects dependencies, put this operation in created, copy it ahead of time, and then compute the properties and rely on the copied data.

data () {
  return {
    tempList: []}},computed: {
  questionList () {
    let list = cloneDeep(this.tempList).map((q, qi) = > {
        // ...
        return q
    })
    return list
  }
},
created () {
  this.tempList = cloneDeep(this.QuestionList)
}
Copy the code

Re-record the flame map:

Sure enough, the svgHeight function is not executed and the time is greatly reduced. Because the calculated property Watcher was not collected for the corresponding configuration field, the calculated property evaluation will not be triggered again if the configuration is changed. So that proves the conjecture above.

The solution

Once you know the bottleneck, the solution is simple. In fact, there are several ideas:

  1. As above, instead of calling cloneDeep inside the calculated property, a copy is made ahead of time to use as a dependency for the calculated property.
  2. QuestionList[index]. XXX can be accessed separately by this.QuestionList[index]. XXX method, allowing the required responsive attributes to collect the dependencies.

However, this part is not the focus of this article, because it is based on business scenarios to solve the problem, the most important thing is to know how to analyze and locate the problem.

conclusion

In this article, I will share the process of using Performance to record the flame chart and locate the problem, and how to think about the problem. A wave of cloneDeep processes and the principle of calculating attributes are also analyzed.

It is also possible to conclude that using cloneDeep to copy reactive data in computed properties can cause dependencies to be over-collected, leading to unexpected updates to computed properties (if that’s what your business is about, never mind, hahaha). The above is the whole process of my investigation of performance problems, hoping to bring some thinking and experience to students.