网络方面

优化资源

代码分割懒加载

1、路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。

Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:

js

// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')

const router = createRouter({
  // ...
  routes: [
    { path: '/users/:id', component: UserDetails }
    // 或在路由定义里直接使用它
    { path: '/users/:id', component: () => import('./views/UserDetails.vue') },
  ],
})

component (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。一般来说,对所有的路由都使用动态导入是个好主意。

注意

不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。

如果你使用的是 webpack 之类的打包器,它将自动从代码分割中受益。

如果你使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 正确地解析语法。

2.把组件按组分块
使用 webpack

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4):

const UserDetails = () =>
  import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
  import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
  import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')

webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。

使用 Vite

在Vite中,你可以在rollupOptions下定义分块:

//vue.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      // https://rollupjs.org/guide/en/#outputmanualchunks
      output: {
        manualChunks: {
          'group-user': [
            './src/UserDetails',
            './src/UserDashboard',
            './src/UserProfileEdit',
          ],
        },
      },
    },
  },
})
3.分割公共组件

不做处理的话,在打包后的dist里可以发现,公共组件的代码重复出现在各个js文件里。

处理方式是使用动态引入,并命名为一个单独的chunk

4.弹窗类需要被触发才显示的组件可以懒加载

这样可以进一步提升页面加载速度,减少白屏时间

合理使用 Tree shaking

Tree shaking 的作用:消除无用的 JS 代码,减少代码体积

举个🌰:

// util.js
export function targetType(target) {
  return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
}
export function deepClone(target) {
  return JSON.parse(JSON.stringify(target));
}

项目中只使用了 targetType 方法,但未使用 deepClone 方法,项目打包后,deepClone 方法不会被打包到项目里

tree-shaking 原理:

依赖于ES6的模块特性,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是 tree-shaking 的基础

静态分析就是不需要执行代码,就可以从字面量上对代码进行分析。ES6之前的模块化,比如 CommonJS 是动态加载,只有执行后才知道引用的什么模块,就不能通过静态分析去做优化,正是基于这个基础上,才使得 tree-shaking 成为可能

Tree shaking 并不是万能的

并不是说所有无用的代码都可以被消除,还是上面的代码,换个写法 tree-shaking 就失效了

// util.js
export default {
  targetType(target) {
    return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
  },
  deepClone(target) {
    return JSON.parse(JSON.stringify(target));
  }
};

// 引入并使用
import util from '../util';
util.targetType(null)

同样的,项目中只使用了 targetType 方法,未使用 deepClone 方法,项目打包后,deepClone 方法还是被打包到项目里

究其原因,export default 导出的是一个对象,无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree-shaking 只对使用 export 导出的变量生效

这也是函数式编程越来越火的原因,因为可以很好利用 tree-shaking 精简项目的体积,也是 vue3 全面拥抱了函数式编程的原因之一

1.用依赖库时采用按需加载,不要全部引入。

2.自己编写工具时采用函数式编程,更好发挥树摇优化

内存方面

合理的降低内存占用,解决内存泄漏,可以提高应用稳定性。

调试内存占用

观察内存占用,可以用Performance API,可以用devtools memory。

查看js堆快照,要重点看三个指标:

  • Retained Size: 对象及其依赖对象的内存大小。
  • Delta 发生变化的全部对象的个数。净增对象个数。
  • Freed Size 新对象释放出的内存空间。
Heap Snapshot中的Constructor
  • (closure) 通过函数闭包对一组对象的引用计数
  • (array、string、number、regexp) 不同对象类型的列表,Array,String,Number,RexExp的属性
  • (已编译代码) 与已编译代码相关的任何内容。
  • HTMLDivElement、HTMLAnchorElement、DocumentFragment DOM对象。
  • Dep、Observer、VNode、Watcher、VueComponent 这些是vue特有的对象。

即使是一个小对象,都可能间接的hold了庞大的内存。从而导致自动GC程序不能处理掉这些被间接hold的内存。

vue是监测不到我们不用某些模块的,只有绑定在vue实例上的实例才会与组件一起销毁,没有绑定的一定要主动销毁。

在利用class Filter去搜索constructor,观察delta size为正数,freed size为0,说明模块有没有没有销毁。

选型前端第三方框架时,不妨先测试测试是否存在内存泄露。

lodash

moment

开发代码层面的优化

console.log导致的内存泄露

只有 devtools 打开时,console 打印才会引起内存泄漏的,如果不打开控制台,console 是不会引起内存变化的。稳妥起见,需要在生产环境时使用插件过滤掉 console 打印

// vue.config.js
const TerserPlugin = require('terser-webpack-plugin') 

module.exports = {
  configureWebpack: (config) => {
    if (process.env.NODE_ENV === 'production') {
    // 返回一个将会被合并的对象
    return {
      optimization: {
        minimizer: [
          new TerserPlugin({
            sourceMap: false,
            terserOptions: {
              compress: {
                drop_console: true
              }}})
        ]}};
}}};

    正确使用闭包

    闭包:简单理解为函数的函数,最常见的场景为:函数作为一个函数的参数,或函数作为一个函数的返回值时。

    1. 闭包的作用是什么?

    1延伸变量作用域范围,读取函数内部的变量

    错误的写法:闭包所引用的变量在函数外部。因为开发者需要非常小心,否则稍有不慎就会造成内存泄露,我们总是希望可以通过 gc 自动回收,避免人为干涉

    正确的写法:闭包引用的变量定义在函数中。这样随着外部引用的销毁,该闭包就会被 gc 自动回收 (推荐),无需人工干涉

    变量如果定义在函数内部,当函数运行完成后,这些内部变量就会完成它的生命周期,能够被垃圾回收,没有必要做手动清除。

    总结来说:应尽量避免定义全局的临时变量,而函数内部仅涉及到本级作用域的变量(不会被闭包保留引用的)无需手动清除。对于全局公共变量、以及闭包内引用变量来说,仅在变量确实使用完毕、值不再需要保留时,才需要手动清除变量的值。

    对于浏览器来说,还需要考虑 DOM 元素的情况。如果某个变量(以及对象类型变量上的属性、元素)保留了对于 DOM 元素的引用,包括引用本页的元素或同源的 iframe 内的元素等情况,当该元素在页面上被删除时,对于它的引用很可能还被保留在变量内,这样就会造成内存泄露。

    使用弱引用weakMap来处理监听事件的注册

    // 代码1
    element.addEventListener('click', handler, false)
    
    // 代码2
    weak.set(element, handler)
    element.addEventListener('click', weak.get(element), false)
    

    代码 2 比起代码 1 的好处是:由于监听函数是放在 WeakMap 里面,一旦 element 对象的其他引用消失,与它绑定的监听函数 handler 所占的内存也会被自动释放

    UI渲染线程方面

    骨架屏优化白屏时长

    使用骨架屏,可以缩短白屏时间,提升用户体验。国内大多数的主流网站都使用了骨架屏,特别是手机端的项目

    SPA 单页应用,无论 vue 还是 react,最初的 html 都是空白的,需要通过加载 JS 将内容挂载到根节点上,这套机制的副作用:会造成长时间的白屏

    常见的骨架屏插件就是基于这种原理,在项目打包时将骨架屏的内容直接放到 html 文件的根节点中

    骨架屏插件

    这里以 vue-skeleton-webpack-plugin 插件为例,该插件的亮点是可以给不同的页面设置不同的骨架屏

    大数据列表虚拟滚动

    首页中不乏有需要渲染长列表的场景,当渲染条数过多时,所需要的渲染时间会很长,滚动时还会造成页面卡顿,整体体验非常不好

    虚拟滚动——指的是只渲染可视区域的列表项,非可见区域的不渲染,在滚动时动态更新可视区域,该方案在优化大量数据渲染时效果是很明显的

    虚拟滚动图例:

    virtual.png

    虚拟滚动基本原理:

    计算出 totalHeight 列表总高度,并在触发时滚动事件时根据 scrollTop 值不断更新 startIndex 以及 endIndex ,以此从列表数据 listData 中截取对应元素

    虚拟滚动插件

    虚拟滚动的插件有很多,比如 vue-virtual-scroller、vue-virtual-scroll-list、react-tiny-virtual-list、react-virtualized 等

    Web Worker 优化长任务

    由于浏览器 GUI 渲染线程与 JS 引擎线程是互斥的关系,当页面中有很多长任务时,会造成页面 UI 阻塞,出现界面卡顿、掉帧等情况

    查看页面的长任务:

    打开控制台,选择 Performance 工具,点击 Start 按钮,展开 Main 选项,会发现有很多红色的三角,这些就属于长任务(长任务:执行时间超过50ms的任务)

    longTask.png

    Web Worker 具体的使用与案例,详情见 一文彻底了解Web Worker,十万、百万条数据都是弟弟🔥

    Web Worker 的通信时长

    并不是执行时间超过 50ms 的任务,就可以使用 Web Worker,还要先考虑通信时长的问题

    假如一个运算执行时长为 100ms,但是通信时长为 300ms, 用了 Web Worker可能会更慢

    比如新建一个 web worker, 浏览器会加载对应的 worker.js 资源,下图中的 Time 是这个资源的通信时长(也叫加载时长)

    load.png

    当任务的运算时长 - 通信时长 > 50ms,推荐使用Web Worker

    Web Worker的限制

    1、在 Worker 线程的运行环境中没有 window 全局对象,也无法访问 DOM 对象

    2、Worker中只能获取到部分浏览器提供的 API,如定时器navigatorlocationXMLHttpRequest

    3、由于可以获取XMLHttpRequest 对象,可以在 Worker 线程中执行ajax请求

    4、每个线程运行在完全独立的环境中,需要通过postMessagemessage事件机制来实现的线程之间的通信

    5.Worker 内部不能直接 import npm 包。如果需要在 Worker 里使用库(如 lodash 或 spark-md5):

    • 下载库的 .min.js 文件。
    • 使用 self.importScripts('/path/to/spark-md5.min.js') 引入

    6.陷阱

    陷阱类型 现象 最佳实践
    过度使用 简单任务变慢 仅用于耗时 > 50ms 的计算密集型任务
    大数据传输 内存翻倍、页面卡死 使用 Transferable Objects (ArrayBuffer)
    频繁通信 UI 依然卡顿 批量聚合消息,减少 postMessage 次数
    资源管理 内存泄漏、CPU 占用高 用完即焚 (terminate) 或使用线程池
    DOM 操作 代码报错 Worker 中绝对禁止操作 DOM,只能通过消息通知主线程

    后续更新中...

    Logo

    腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

    更多推荐