优化过程

总的策略是:对于所有执行时间长的组件,其子组件采用分片挂载。

首先,我们定义一个通用的分片执行方法:

/**
 * 分片执行任务列表,避免一次性执行造成 JS 阻塞
 * 优先使用 requestIdleCallback,不支持则使用 setTimeout
 * @param {Array<function>} tasks 任务列表
 */
export function fiberExecute(tasks) {
  const _tasks = [...tasks];
  const fun = () => {
    // 去除第一个任务
    const task = _tasks.shift();
    if (task) {
      // 如果是函数,执行
      typeof task === 'function' && task();
      // 还有任务,继续加入事件循环
      if (_tasks.length) {
        myRequestIdleCallback(fun);
      }
    }
  };
  myRequestIdleCallback(fun);
}

function myRequestIdleCallback(fun) {
  if (window.requestIdleCallback) {
    requestIdleCallback(fun);
    return;
  }
  setTimeout(() => {
    requestIdleCallback(fun);
  }, 0);
}

对全局组件,采用分片挂载:

this.keys.map(k => () => (this[k] = true));:创建一个函数数组 tasks,每个函数的功能是将当前实例中的一个属性设置为 true。这些属性名存储在 this.keys 数组中。这可能是用于控制其他组件的显示或状态。

// 使用 v-if 控制先不挂载
<component-config v-if="isShowComponentConfig" />

// global-dialogs mounted 后分片挂载子弹框
mounted() {
  // 全局弹框太多,一次性渲染需要耗时500ms+,会造成JS阻塞,这里采用分片渲染
  const tasks = this.keys.map(k => () => (this[k] = true));
  fiberExecute(tasks);
}

组件注册分片执行。由于这里单个组件执行时间一般不超过 5ms,为了尽快完成组件挂载,这里采用一次挂载 6 个组件,粒度可以自己控制。也就是分片策略的粒度,我们可以自己控制。

import { fiberExecute } from 'utils/fiber';

export function installComponents(_vue, components) {
  const keys = components.keys();
  // 每次处理个数
  const len = 6;
  const keysArr = [];
  let cur = 0;
  while (cur < keys.length) {
    keysArr.push(keys.slice(cur, cur + len));
    cur += len;
  }

  const tasks = keysArr.map(ks => () => {
    ks.forEach(key => {
      // 注册逻辑
    });
  });
  fiberExecute(tasks);
}

还有插件注册、feedback 按钮挂载时机等所有高耗时的任务都可以使用分片执行策略。

目前的分片执行最小粒度为组件,如果单个组件执行过长,还可以继续对该组件进行拆分。

采用分片执行后,可以看到 JS 任务执行时间基本是均匀,避免了出现大的任务块,一直占用渲染进程的情况。

补充

补充:分片执行(chunked execution)是「长任务分批调度」的常见思路,核心是借助浏览器的空闲回调或微任务队列,避免一次性同步逻辑占用主线程。