[TOC]

https://mp.weixin.qq.com/s/xccL9lTY6GKY2YqX7_FDQw

1、服务开启 gzip

为了减少数据在网络上的传输时间,可以启用gzip压缩。gzip压缩是属于时间换空间的做法

具体是否开启,视自己项目情况而定

gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
gzip_disable "MSIE [1-6]\.";

2、前端打包开启gzip

如果 静态资源服务 没有开启 gzip 的话 ,前端在打包的时候,可以开启 gzip ,这样在打包的文件中会生成 .gz 的文件

配置如下:

// webpack.build.js
// 开启 gzip 压缩
const CompressionWebpackPlugin = require("compression-webpack-plugin");
// gzip 压缩匹配规则
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i; 
module.exports = {
        ...,
      plugins: [
        config.build.productionGzip && new CompressionWebpackPlugin({
            filename: "[path].gz[query]",
            algorithm: "gzip",
            test: productionGzipExtensions,
            threshold: 10240,
            minRatio: 0.8
        }),
    ]
}

3、前端打包分析第三方包体积

webpack-bundle-analyzer 可以分析出打包之后 各个模块所占用的体积,方便我们知道,项目中哪些模块占得体积大,哪些模块不怎么占体积

配置以下命令之后 就可以运行 npm run analyze 查看分析结果

配置如下:

// webpack.build.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
        ...,
      plugins: [
            // 打开打包结果分析工具
          process.env.npm_config_report && new BundleAnalyzerPlugin(),
    ]
}

// package.json
{
    ...,

  "scripts": {
          "analyze": "cross-env npm_config_report=true npm run build",
  },
  ...,    
}

4、前端进行包抽离

总所周知,我们 npm inode_module 的包,如果没有进行按需导入的话,大概率,会被打包到 vendor.js 中,这也就导致 vendor.js 弄不好 会有好几兆,这样一个大体量的入口文件会导致首次渲染白屏时间过长.

所以,需要将不怎么需要更新的包,在打包的时候,进行抽离。 这样。在 HTML 中,可以使用引入 CDN 加速之后的 npm 包资源

配置如下

// webpack.base.js
module.exports = {
     externals: { // 使用外链引入的npm包
        jszip: 'JSZip',
        xlsx: 'XLSX',
        'text-encoding': 'TextEncodingPolyfill'
    },
}

5、H5图片优化

5.1 CDN

如果有 CDN 图床服务,可以开启CDN 图床尺寸大小压缩功能

然后利用 img 标签的 srcset/sizes 属性和 picutre 标签实现响应式图

就可以实现,在不同分辨率下,加载不同像素的图片

具体做法:

// srcset 可以接受一段字符串,用来定义一个或多个图像候选地址,以 ,分割,每个候选地址将在特定条件下得以使用。
// 候选地址包含图片 URL 和一个可选的宽度描述符和像素密度描述符,该候选地址用来在特定条件下替代原始地址成为 src 的属性

<div class="box">
  <img src="/files/16797/clock-demo-200px.png"
       alt="Clock"
       srcset="/files/16864/clock-demo-200px.png 1x, /files/16797/clock-demo-400px.png 2x">
</div>

5.2 非CDN

处理方式主要是要控制好图片懒加载的逻辑(如 onload 后再加载),可以借助各类 lazyload 的库去实现。H5项目用的是位置检测( getBoundingClientRect )图片到达页面可视区域再展示。

简易的纯原生懒加载实现:

function lazyLoad(){
    const imageToLazy = document.querySelectorAll('img[data-src]');
    const loadImage = function (image) {
        image.setAttribute('src', image.getAttribute('data-src'));
        image.addEventListener('load', function() {
            image.removeAttribute("data-src");
        })
    }


    const intersectionObserver = new IntersectionObserver(function(items, observer) {
        items.forEach(function(item) {
            if(item.isIntersecting) {
                loadImage(item.target);
                observer.unobserve(item.target);
            }
        });
    });

    imageToLazy.forEach(function(image){
        intersectionObserver.observe(image);
    })
}

6、前端工程化优化 css

很多时候,一个css文件,首屏需要用到的可能只有整个文件的 5% 不到,但是却要加载整个 css 文件,及其的影响页面渲染速度

这个时候就可以考虑对页面首屏的关键 CSS 进行内联,让页面渲染不被CSS 阻塞,再把完整 CSS 加载进来。

实现这个功能的插件叫 critters-webpack-plugin github地址为 https://github.com/GoogleChromeLabs/critters

安装:

npm i -D critters-webpack-plugin

使用:

// webpack.config.js
const Critters = require('critters-webpack-plugin');

module.exports = {
  plugins: [
    new Critters({
           // 输出: <link rel="preload" onload="this.rel='stylesheet'">
          preload: 'swap',
          // 不要内联关键字体规则,而是预加载字体 URL:
          preloadFonts: true
    })
  ]
}

7、路由页面按需加载

引入页面 配置路由的时候,使用 ES6 的 import() 动态按需导入文件 不仅仅是 页面文件,比如 某一个插件 只有那一个页面,某一个场景下,才能使用,那么就可以使用 import(‘xxxx/xxx/‘).then()

// 页面路由配置
const Demo = () => import ('@/pages/demo/demo.vue');

// 页面内按需加载文件
const btn = document.querySelector('.demand')
btn.onclick=()=>{
    import('jszip').then(res=>{
      // 业务代码
  }
}

8、css相关的性能优化

8.1 页面渲染过程

  • **html** 代码被 **HTML** 解析器解析成 **DOM**
  • **css** 代码被 **css** 解析引擎解析成 **css** 样式树
  • **css** 样式树和 **DOM**树整合,生成渲染树
  • 将根据渲染树样式,进行计算,生成最终的布局
  • 浏览器引擎将计算后的布局,绘制出来,呈现在用户面前

前端性能优化 - 图1

8.2 页面渲染小常识

  • CSS 是页面渲染的关键因素之一,(当页面存在外链 **CSS** 时,)浏览器会等待全部的 **CSS** 下载及解析完成后再渲染页面。关键路径上的任何延迟都会影响首屏时间,因而我们需要尽快地将 **CSS** 传输到用户的设备,否则,(在页面渲染之前,)用户只能看到一个空白的屏幕。
  • 网页生成的时候,至少会渲染一次。
  • 在用户访问的过程中,还会不断重新渲染。
  • 重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)。
  • 重排重绘 影响大

8.2.1 重排(回流):

DOM 的变化影响了元素的几何信息( DOM 对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。

引起重排的原因:
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,例如:

  • 添加或者删除可见的 DOM 元素;
  • 元素尺寸改变——边距、填充、边框、宽度和高度
  • 内容变化,比如用户在 input 框中输入文字
  • 浏览器窗口尺寸改变—— resize 事件发生时
  • 计算 offsetWidthoffsetHeight 属性
  • 设置 style 属性的值

8.2.2 重绘:

当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。

8.2.2.1 引起重绘的一些属性:
前端性能优化 - 图2

8.2.3 浏览器渲染机制

当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。

 div.style.width = "30px";
 div.style.height = "30px";
 div.style.left = "30px"; 
 div.style.top = "30px";
//这只会渲染一次

强制刷新渲染队列:比如我们在渲染过程中,获取或者计算样式信息,无论何时浏览器都会立即执行渲染队列的任务,即使该值与你操作中修改的值没关联。

div.style.width = "30px";
console.log( div.style.width );
div.style.height = "30px";
console.log( div.style.height );
div.style.left = "30px"; 
console.log( div.style.left )
div.style.top = "30px";
console.log( div.style.top );
 //渲染四次

8.2.4 影响渲染性能的点
  • 重排
  • 浏览器直到渲染树构建完成后才会渲染页面;
  • 渲染树由 DOM 与 CSSOM 组合而成;
  • DOM 是 HTML 加上(同步)阻塞的 JavaScript 操作(DOM 后的)结果;
  • CSSOM 是 CSS 规则应用于 DOM 后的结果;
  • 使 JavaScript 非阻塞非常简单,添加 async 或 defer 属性即可;
  • 相对而言,要让 CSS 变为异步加载是比较困难的;

所以记住这条经验法则:(理想情况下,)最慢样式表的下载时间决定了页面渲染的时间。

8.3 基于上面的一些基础知识可以总结一下 css 优化性能的点

8.3.1 减少重排

8.3.1.1 分离读写操作
 div.style.width = "30px";
 div.style.height = "30px";
 div.style.left = "30px"; 
 div.style.top = "30px";

 console.log( div.offsetLeft )
 console.log( div.offsetheight );
 console.log( div.offsetwidth );
 console.log( div.offsettop );

8.3.1.2 样式集中改变
 div.style.width = "30px";
 div.style.height = "30px";
 div.style.left = "30px"; 
 div.style.top = "30px";

8.3.1.3 缓存布局信息
 // bad 
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
// good 缓存布局信息 相当于读写分离
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';

8.3.1.4 离线改变dom
  1. 隐藏要操作的dom
  2. 通过使用 **DocumentFragment** 创建一个 **dom** 碎片,在它上面批量操作 **dom** ,操作完成之后,再添加到文档中,这样只会触发一次重排。
  3. 复制节点,在副本上工作,然后替换它

8.3.1.5 position 属性为 absolutefixed

当该元素设置了定位之后,就会脱离文档流,当再改变该元素的样式的时候,就只是局部重排了。对大体上的影响很小

8.3.1.6 优化动画

启用 **GPPU** 加速:Canvas2D,布局合成, CSS3 转换(transitions ),CSS3 3D 变换(transforms),WebGL和视频(video);

8.3.2 优先加载关键 CSS,懒加载其他 CSS;

找出首次渲染所需的样式(通常是首屏相关的样式),将它们内联到 标签中,其他样式则通过异步的方式进行加载。

8.3.3 根据媒体类型拆分代码

根据媒体查询拆分 CSS 文件,这样浏览器就会:

  1. 以非常高的优先级下载符合当前上下文(设备、屏幕尺寸、分辨率、方向等)的 CSS 文件;
  2. 阻塞关键路径;
  3. 以非常低的优先级下载不符合当前上下文的 CSS 文件,不会阻塞关键路径。
<link rel="stylesheet" href="all.css" media="all" />
<link rel="stylesheet" href="small.css" media="(min-width: 20em)" />
<link rel="stylesheet" href="medium.css" media="(min-width: 64em)" />
<link rel="stylesheet" href="large.css" media="(min-width: 90em)" />
<link rel="stylesheet" href="extra-large.css" media="(min-width: 120em)" />
<link rel="stylesheet" href="print.css" media="print" />

前端性能优化 - 图3
浏览器仍然会下载全部的 CSS 文件,但只有符合当前上下文的 CSS 文件会阻塞渲染。

8.3.4 避免使用 @import

在 HTML 文档中应该避免使用 @import,在 CSS 文件中更应避免使用 @import,以及警惕预加载扫描器的怪异行为。
**@import** 渲染过程:

  1. 下载 HTML;
  2. 请求并下载依赖的 CSS;下载及解析完成后,本该是构造渲染树,然而;
  3. CSS 依赖了其他的 CSS,继续请求并下载 CSS 文件;
  4. 构造渲染树。
  • 如果你没有包含 **@import****CSS** 文件的修改权限,为了让浏览器并行下载 **CSS** 文件,可以往 **HTML** 中补充相应的 **<link rel="stylesheet" src="@import的地址" />**。浏览器会并行下载相应的 **CSS** 文件且不会重复下载 **@import** 引用的文件。
  • **HTML** 中使用 **@import**,在以 **WebKit****Blink** 为内核的浏览器中,可能会触发它们预加载扫描器的 bug,在 **Firefox****IE/Edge** 中,则表现低效。

8.3.5 根据项目方案决定 CSS文件和 JavaScript 文件的加载顺序
  1. CSS 文件后的 JavaScript 仅在 CSSOM 构建完成后才会执行;如果你的 JavaScript 不依赖 CSS;将它放置于 CSS 之前;
  2. 如果 **JS** 文件没有依赖 **CSS**,你应该将 **JS** 代码放在样式表之前。 既然没有依赖,那就没有任何理由阻塞 **JavaScript** 代码的执行。

8.3.6 仅加载 DOM 依赖的 CSS:这将提高初次渲染的速度使让页面逐步渲染。
<html>
  <head>
      <link rel="stylesheet" href="core.css" />
  </head>
  <body>
          <link rel="stylesheet" href="site-header.css" />
          <header class="site-header">
                  <link rel="stylesheet" href="site-nav.css" />
                  <nav class="site-nav">...</nav>
          </header>
          <link rel="stylesheet" href="content.css" />
          <main class="content">
          <link rel="stylesheet" href="content-primary.css" />
          <section class="content-primary">
            <h1>...</h1>
            <link rel="stylesheet" href="date-picker.css" />
            <div class="date-picker">...</div>
          </section>
          <link rel="stylesheet" href="content-secondary.css" />
          <aside class="content-secondary">
             <link rel="stylesheet" href="ads.css" />
             <div class="ads">...</div>
          </aside>
          </main>
          <link rel="stylesheet" href="site-footer.css" />
          <footer class="site-footer"></footer>
  </body>
</html>

这样的结果是我们能逐步渲染页面,当前面的 CSS 可用时,页面将呈现对应的内容,(而不需等待全部 CSS 下载并解析完毕)

9、网络方面

9.1 网络请求方面。推荐使用 HTTP2 协议

9.1.1HTTP2协议相对 HTTP1 优点:
  1. 协议头压缩,更小的负载体积。
  2. 多路复用,支持 N 条请求并发请求。
  3. 请求优先级,更快的关键请求。(Web 性能优化:控制关键请求的优先级 翻译自:https://calibreapp.com/blog/critical-request,作者 Ben Schwarz,

    9.1.2HTTP2协议相对 HTTP1 可能会废弃的优化方案
  4. 资源合并。如 https://shanyue.tech/assets??index.js,interview.js,report.js

  5. 域名分片。
  6. 雪碧图(将无数小图片合并成单个大图片)。

控制面板查看协议方式:

  1. 打开控制面板,找到network选项
  2. 以下任意右键,勾选 Protocol 再请求,就可以看到当前的请求是走的 什么协议了!!!

image.pngimage.png

9.2 充分利用 HTTP缓存

指定一定的缓存策略,对于 CDN来讲可减少回源次数,对于浏览器而言可减少请求发送次数。无论哪一点,对于二次网站访问都具有更好的访问体验。

9.2.1 缓存策略

9.2.1.1 强缓存

打包后带有 hash 值的资源 (如 /build/a3b4c8a8.js)

9.2.1.2 协商缓存

打包后不带 hash 值的资源 (如 /pages/index.html)

9.2.2 分包加载(bundle spliting)

避免一行代码修改导致整个 bundle 的缓存失效

9.3 请求资源体积务求更小

9.3.1 代码采用压缩混淆工具
  1. terser: terser ,如果需要集成 webpack 的话可以使用 terser-webpack-plugin。对于 terser的压缩速率,具体可以查看 https://try.terser.org/
  2. swc:swc,这个是使用 rust语言写的,具备更高的性能,拥有与 terser 相同的 API
  3. HTMLMinifier:HTML 代码压缩工具。具体可以查看 https://github.com/terser/html-minifier-terser

9.3.2 图片压缩
  1. 尽量使用 avif格式。前端发展的现在,webp普遍比 jpeg/png 更小,而 avif 又比 webp小一个级别

9.3.3 自定义资源加载优先级(preload / prefetch
  1. preload加载当前路由必需资源,优先级高。一般对于 Bundle Spliting 资源与 Code Spliting 资源做 preload
  2. prefetch优先级低,在浏览器 idle 状态时加载资源。一般用以加载其它路由资源,如当页面出现 Link,可 prefetch 当前 Link 的路由资源。(next.js 默认会对 link做懒加载+prefetch,即当某条 Link 出现页面中,即自动 prefetchLink指向的路由资源)
    <link rel="prefetch" href="style.css" as="style">
    <link rel="preload" href="main.js" as="script">
    

    10、渲染优化

    10.1 长列表优化

    对于长列表的页面,由于页面需要渲染大量的 DOM这会导致页面变的特别卡顿。

对于以上的问题,于是乎出了两种解决方案:

  1. 只渲染可视区域
  2. 分片分批次渲染固定的数量的页面元素。

10.1.1 渲染可视区域

既然数据很多,但是用户每次只能看到部分的数据,那完全可以只渲染能看到的列表数据,对于看不见的列表,先不进行渲染,这就是 “虚拟列表优化”

插件介绍:
对于 react 项目,可以采用 [react-virtualized](https://github.com/bvaughn/react-virtualized)[react-window](https://github.com/bvaughn/react-window)
对于 vue项目,可以使用 vue-virtual-scroll-listvue-virtual-scroller

实现原理:

// VirtualList.vue
<template>
  <!-- 展示区域 -->
  <div class="wrap" ref="wrap" @scroll="handleScroll">
    <!-- 为了显示滚动条 -->
    <div ref="scrollHeight"></div>
    <!-- 展示的内容 -->
    <div class="visible-wrap" :style="{transform: `translateY(${offset}px)`}">
      <div v-for="item in visibleData" :key="item.id" :id="item.id">
        <slot :item="item"></slot>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VirtualList',
  props: {
    size: Number,
    keeps: Number,
    arrayData: Array
  },
  data() {
    return {
      start: 0,
      end: this.keeps,
      offset: 0 // 列表内容的偏移量
    }
  },
  computed: {
    visibleData() {
      return this.arrayData.slice(this.start, this.end)
    }
  },
  mounted() {
    this.$refs.scrollHeight.style.height = this.arrayData.length * this.size + 'px'
    this.$refs.wrap.style.height = this.keeps * this.size + 'px'
  },
  methods: {
    handleScroll() {
      const scrollTop = this.$refs.wrap.scrollTop
      // 计算从下标为几的一项开始渲染,减 1 是因为渲染的数据是从第 0 项开始的
      this.start = Math.ceil(scrollTop / this.size) - 1 >= 0 ? Math.ceil(scrollTop / this.size) - 1 : 0
      this.end = this.start + this.keeps
      // 当列表向上(下)滚动时,为了让渲染的列表一直处于可视范围内,就要把列表向下(上)挪
      this.offset = this.start * this.size
    }
  }
}
</script>


<style scoped lang="less">
.wrap {
  position: relative;
  overflow-y: scroll;
}

.visible-wrap {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
}
</style>
  1. 每一项 **Item** 高度固定
    1. 列表的数据来源是一个长度为 1000 的简单的 list 数组
    2. 我们的目标是只需要传递 3 个值给 VirtualList 组件,就可以正常使用:
      • size:每一项的高度
      • keeps:希望展示几条数据
      • arrayData:列表数据
    3. VirtualList 组件里得有 3 个部分:
      • 最外层容器区域。高度固定,超出区域出现滚动条,高度为传入的 size 乘上 keeps
      • 列表本应该有的高度区域,也就是列表如果全部渲染的总高度。因为只渲染 keeps 指定的条数的数据,就会导致没有滚动条或滚动条无法起到预告总的列表长度的功能,所以要用一个高度为列表总长度的 div 让滚动条正确显示;
      • 要展示的内容。展示的数据应该是总数据 arrayData 的某一部分。展示的数据 item 还得传给父组件,在父组件进行使用,这里就用到了插槽。
    4. 当滚动列表时(handleScroll 触发),我们要及时的根据滚动的距离更新应该显示的数据:
      • onscroll 处理的是对象内部内容区的滚动事件,所以是对最外部固定高度的 wrap 容器进行监听。
      • 如下图所示:蓝色矩形为可视区域,假设传入的 keeps 为 3 ,当滚动列表(红色矩形)时,渲染的列表区域,也就是 3 个 item(深蓝绿色矩形) 占据的区域也会跟着滚动,如果仅仅改变渲染的内容,也就是根据滚动距离从 item1 开始渲染,那么此时这个 item1 就会替换下图的 item0 ,位于可视区域之外,无法被看见。

66276e6410b94fbe95e748dfb86b5d98_tplv-k3u1fbpfcp-watermark (1).jpg
所以需要根据已经滚动出可视区域的 item 的个数和每一项 item 的高度的乘积(offset)进行反向的移动,移动的距离为 this.start * this.size。注意: offset 的值在多数情况下不会等于 scrollTop 的值。
c61f68be80ac4f44a38cc46c79d90b54_tplv-k3u1fbpfcp-watermark.jpg

注:一个元素的 scrollTop 值是这个元素的内容顶部到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为 0。

解决滚动时,如果刚好渲染的第一项只显示了部分,那么可视区域的最底下就会出现相应高度的空白的问题,就如下图中,可以看到右侧滚动条的高度超过 23 那一项的区域为空白:
79803740f20c444d973d01b5ef2f2005_tplv-k3u1fbpfcp-watermark.jpg
解决方案:在原先的渲染项数基础上,再多向前和后渲染若干项,那么决定渲染哪些数据的 visibleData 的计算就会发生变化,原先是定义了 startend 用于标记到底切割哪一部分,代码如下:

visibleData() {
  return this.arrayData.slice(this.start, this.end)
}

现在则是新定义了 prevCountrenderStartnextCountrenderEnd 4 个参数,另外 offset 也需要改变为 (this.start - this.prevCount) * this.size,因为假设原本渲染 8 项,往上滚动了 1 项的距离,那么渲染的 8 项由 0 ~ 7 变为 1 ~ 8,0 项被删除,这时需要把 class="visible-wrap" 的这个 div 往下移动 1 项,才能刚好在可视区域的顶部看到第 1 项;现在则是往上移动 1 项时,class="visible-wrap" 这个 div 里渲染的项数就会变为 1+8+8=17 项,0 项不会被删除,不需要再往下移动 1 项,所以会有 this.start - this.prevCount

  1. 每一项 **Item** 高度不固定 ```vue // VirtualList.vue

原来对于列表如果全部渲染应该有的高度的计算 `this.arrayData.length * this.size + 'px'` 显然不合适了,因为每项 `item` 的高度 `size` 不确定了。在开始重新计算之前,先介绍一下二分法:

- **二分法:**
   - 使用前提:数组已经按升序排列
   - 基本原理:首先将要查找的值(`value`)同数组中间那一项的值(`midValue`)进行比较,当 `start <= end`
      1. 如果` value < midValue`,则 `end = midValue - 1`,只需要在数组的前一半元素中继续查找
      2. 如果` value = midValue`,匹配成功,查找结束
      3. 如果 `value > midValue`,则 `start = midValue + 1`,只需要在数组的后一半元素中继续查
      4. 如果 `while` 循环结束后都没有找到 `value`,返回 -1
```javascript
const arr = [-1, 5, 6, 12, ...]
const start = 0,
end = arr.length -1,
midValue = start + (end - start) / 2

注意: 在二分法中,计算中间项的索引时用的是 midValue = start + (end - start) / 2 而不是直接使用更简单的公式 midValue = (start + end) / 2,是为了防止值溢出的情况,因为 start + end 的值可能会大于 js 最大的能表示的数。(如果 start < 0end < 0时,end - start 也可能会溢出)

利用二分法重新计算 **start**

  1. 在页面加载完毕后,对数据数组里每一项的 height, topbottom 的值做个缓存(此时的 size 为我们预估的,滚动条的高度并不准确),存放在数组 positionListArr 里;
  2. 用二分法开始查找,我们页面滚动的距离 scrollTop 对应于 positionListArr 里的哪一项的 bottom 的值。之所以用二分法是因为后面会根据真实 dom 重行计算每一项的 height, topbottom,到时候每一项的 size 就可能不一样了;
  3. 之后对于 endoffset 计算原理就跟 item 高度固定的情况一样了。

页面更新后:
页面渲染完成后,获取到真实的 dom,更正缓存在 positionListArr 里的数据,实现更新滚动条的高度。这部分代码用到了 refgetBoundingClientRect 的相关知识:

  • 如果 ref 是写在 v-for 的元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。
  • Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置,除了 widthheight 以外的属性是相对于视图窗口的左上角来计算的,如下图:

463e0812237e4c9f9002cbae6d081c7f_tplv-k3u1fbpfcp-watermark.jpg
代码源码:https://github.com/chaimHL/vue-long-list-optimization

10.1.2 分片渲染

因为 requestAnimationFrame 或定时器是一个宏任务,所以每执行一次 GUI 渲染后就执行一次相关的回调,也就实现了每次添加 50 个 li 节点,从而达到了分片加载的目的。现在加载时间则如下:

const time = Date.now()
/**
 * index: 记录循环到哪了
 * id: 往 li 里添加的内容
 */
let index = 0, id = 0
function load() {
  index += 50
  if (index < 10000) {
    requestAnimationFrame(() => { // 用 requestAnimationFrame(也是宏任务)代替了 setTimeout,性能更好点
      const fragment = document.createDocumentFragment() // IE 浏览器需要使用文档碎片,一般可不用
      for (let i = 0; i < 50; i++) {
        const li = document.createElement('li')
        li.innerText = id++
        fragment.appendChild(li)
      }
      list.appendChild(fragment)
    })
    load()
  }
}
load()
console.log(Date.now() - time)
setTimeout(() => {
  console.log(Date.now() - time)
})

与分片加载之前对比,确实快了许多。但这种方案有个问题:会导致页面的 dom 元素过多,依旧容易造成卡顿。

11、线程优化

11.1 webWorker

有时候一些实时代码编译及转换 如果纯碎使用传统的 Javascript 实现,将会耗时过多阻塞主线程,有可能导致页面卡顿。

如果使用 Web Worker 交由额外的线程来做这件事,将会高效很多,基本上所有在浏览器端进行代码编译的功能都由 Web Worker 实现。MDN 关于 webWorker 的介绍