Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

性能优化 #4

Open
Edwardzerb opened this issue Nov 11, 2020 · 0 comments
Open

性能优化 #4

Edwardzerb opened this issue Nov 11, 2020 · 0 comments

Comments

@Edwardzerb
Copy link
Owner

性能领域的术语概念

F:first 的缩写 ,意思第一次
P: paint 的缩写,意思为绘制
C:contentful,意思为内容

FP(First Paint):首次绘制,页面在屏幕上首次发生视觉上变化的事件

FCP(First Contentful Paint):首次内容绘制,浏览器第一次在屏幕上渲染内容

FMP(First Meaniful Paint):首次有效绘制,表示页面的 “主要内容” 开始出现在屏幕上的时间点,该指标是测量用户加载体验的主要指标

LCP(Large Contentful Paint):表示可视区域中 内容 最大的可见元素开始出现在屏幕上的时间点

TTI(Time To Interactive):可交互时间,网页中第一次 完全达到可交互状态 的时间点。主线程的任务均不超过 50 毫秒(time slicing)。

TTFB(Time To First Byte):浏览器接受第一个字节时间

FCI(First CPU Idle):CPU 第一次空闲的时间,CPU空闲,说明主线程已经空闲下来了,此时就可以接受用户的响应了

FID(First Input Delay):首次输入延迟,可在TTI前开始与网页产生交互,也可能在TTI之后才与网页产生交互
可在 head 标签里注册一个事件(click、mousedown、keydown、touchstart、pointerdown),事件响应函数中使用当前时间减去时间对象被创建的时间

FP与FCP 的区别

  • FP重点是在于视觉上的变化,无论什么内容
  • FCP 指的是 浏览器首次绘制来自 DOM 的内容。例如:文本、图片、SVG、canvas
  • 两个时间可能是一致的,也有可能是 先 FP 后 FCP

关键资源:阻塞网页首次渲染的资源(FP)

DOM的生命周期

  • domLoading:表示开始解析第一批收到的 HTML 文档的字节
  • domInteractive:表示完成全部 HTML 的解析并且 DOM 构建完毕,defer 延迟的脚本就在这个阶段后,domContentLoaded前执行
  • domContentLoaded:HTML 已经加载、解析完毕
    如果没有解析器阻塞 JavaScript,DOMContentLoaded 事件会在 domInteractive 之后立即执行,defer 延迟的脚本就会阻塞
    大多 JavaScript 框架 会在执行自己的逻辑前等待这个事件的触发
  • domComplete:所有的处理都已经完成并且所有的附属资源都已经下载完毕
  • loadEvent:作为网页加载的最后一步以便触发附加的应用逻辑

前端流程

下面的流程就是前端的基本流程,根据这个流程,我们可以看看每一个阶段都可以做哪些性能优化

  • 项目打包编译
  • 输入 url 到 FP
    • DNS
    • 浏览器缓存
    • HTTP
    • CDN
    • 渲染路径(HTML、CSS、JavaScript)
  • FP/FCP
  • FMP
  • LCP
  • TTI
  • FCI

导致性能低的原因

  • 长任务
    • 由于js是单线程的,浏览器同一时间内只能执行一个任务
    • 所以,需要避免任务执行时间超过50ms
  • 像素管道
    • 保证每16.7 秒必须有一帧传输到屏幕上
  • 布局抖动
    • 原本像素管道中的每一步都是异步执行的,如果变成了设置了宽高,然后马上获取,就会变成同步
    • 其实导致这个发生原理就是因为把渲染路径从原本的异步变成了同步
  • 丢帧
    • 每个16.7ms就要输出一帧,要保证每帧都有输出就使用 requestAnimationFrame

解决方案

1.打包构建

1.1 预编译

类似vue,最终都是将tempalte编译成render 函数,预编译就可以省去在运行时才将template 编译成render函数

tree-shaking,在构建过程中,清楚无用代码,可以减少构建后文件的体积

/**
 * tree shaking: 去除无用代码(js、css)
 *   前提:1.必须使用ES6模块化
 *        2.开启production环境
 *   作用:减少代码体积
 *
 *  package.json中配置
 *    "sideEffects": false 所有代码都没有副作用(都可以进行 tree shaking)
 *      问题:可能会把 css/@babel/polyfill(副作用)文件干掉
 *    "sideEffects": ["*.css", "*.less"]
 */

1.2 code Spliting

两种方式:

  • 分离业务代码
  • 第三方库、按需加载
    // 在webpack 配置文件里面配置
  /**
   * 单入口:
   *  可以将 node_modules 中代码单独打包一个chunk 最终输出
   * 多入口:
   *  自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk,并且都引用这个chunk
   */
optimization: {
    splitChunks: {
      chunks: 'all',
      // 分割的 chunk 最小为30kb
      miniSize: 30 * 1024,
      // 没有最大限制
      maxSize: 0,
      // 要提取的chunk最少被引用1次
      minChunks: 1,
      // 按需加载时并行加载的文件的最大数量
      maxAsyncRequests: 5,
      // 入口js文件最大并行请求数量
      maxInitialRequests: 3,
      // 名称连接符
      automaticNameDelimiter: '~',
      // 可以使用命名规则
      name: true,
      cacheGroups: {
        // 分割chunk的组
        // node_moudules文件会被打包到 vendors 组的chunk中 --> vendors~xxx.js
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          // 优先级
          priority: -10
        },
        default: {
          // 要提取的chunk最少被引用2次
          minChunks: 2,
          // 优先级
          priority: -20,
          // 如果当前要打包的模块,和之前已经被提取的模块是同一个,就会复用,而不是重新打包
          reuseExistingChunk: true
        }
      }
    }
  }
  

1.3 import 按需加载

// 按需加载 import
/**
 * 在不配置多入口的情况下,如何将引用的js文件打包成独立的文件
 * 通过js代码,让某个文件被单独打包成一个chunk
 * import 动态导入语法:能将某个文件单独打包
 */

 import(/* webpackChunkName: 'test' */ './print)
  .then(({ mul, count }) => {
    // 文件加载成功
    console.log(mul(2, 5));
  })
  .catch(() => {
    console.log('文件加载失败')
  });

1.4 ssr 渲染 ? dll?

2.浏览器缓存

浏览器缓存:通过 HTTP 请求获取到的资源缓存在浏览器,分为强缓存、协商缓存;这是一个递进关系,先强缓存,缓存不命中再到协商缓存。

HTTP 1.0

  • 强缓存:Expires
  • 协商缓存:If-Modify-Since / Last-Modify

HTTP 1.1

  • 强缓存:Cache-Control
  • 协商缓存:If-none-match/ ETag

详情可看我另一篇文章:浏览器缓存

3.DNS

DNS(Domain Name System):域名系统,域名和 IP 地址相互映射的一个分布式数据库。

DNS Prefetching

浏览器会根据自定义的规则,先提前去解析后面可能会使用到的域名,提前解析,不需要等到要去加载资源的时候再去解析。默认的情况下,网页里的 a 标签里的href属性带的域名会自动去启用DNS Prefetching(不需要在 link 里手动设置),HTTPS 下该默认规则无效。HTTPS 下可以在 meta 标签上操作

<link rel="dns-prefetch" href="//test.com">

// 让 a 标签在 HTTPS 下也能使用 dns-prefetch
<meta http-equiv="x-dns-prefetch-control" content="on">

注意

  • 如果做了 js、服务端 重定向,没有在 link 手动设置,是不起作用的
  • DNS 预解析适合使用在网站里引用了大量其它域名的资源;如果所有资源都在单个域名下,Chrome 就已经做了DNS的缓存

4.HTTP

当强缓存没有命中的时候,就要去服务端获取数据。一个TCP连接下,(chrome下)同个域名的HTTP请求最大并发连接数是6个,多处的请求需要排队等候;其它的浏览器也都有限制。

有一个衡量网络性能的指标,RTT(Round Trip Time),客户端到服务端的往返时延,从发送端发送数据开始,到发送端收到来自接收端的确认,总共的耗时;TCP的传输大小也是有限制,一个RRT只能传输14KB的资源,对此我们要对这个RTT进行优化。

这个阶段从三个点来优化:

  • 减少关键资源个数
  • 降低关键资源大小
  • 降低关键资源的RTT次数

如何减少关键资源的个数

  • 首先,TCP连接下同个域名的HTTP请求个数最大为6,那么我们可以把一些静态资源通过CDN的方式来获取,不在同一个域名下
  • JavaScript 没有操作 DOM 或者 CSSOM 的可以使用defer、async
  • 不是在构建页面之前就要加载的 CSS ,添加媒体取下阻止显现标志
  • 以上两个操作就可以把资源变成非关键资源

如何减少关键资源的大小

  • 打包的时候,压缩 CSS 、Javascript 资源
  • 打包时,去掉js、css、html的注释

如何减少RTT的次数

影响 RTT 的因素,就是资源大小,资源数量,所以方式就是使用上面的两种方式结合。

3.CDN

CDN(Content Delivery Network):内容分发网络,由分布在不同地理位置的 Web 服务器组成。当服务器的地理位置距离我们越远,那传播的延迟就越高;而 CDN 就是让服务器距离客户端更近。

GLSB(全局负载均衡系统):

SLB(本地负载均衡系统):

使用了 CDN 作为缓存的流程:

  • 浏览器在缓存里面查找有没有DNS缓存,没有就继续在操作系统中找,再没,就向本地DNS发出请求
  • 本地DNS逐级向根服务器、顶级域名服务器、权限服务器发出请求,直到得到GLSB的IP地址
  • 本地DNS向GLSB发出请求,GLSB主要是根据本地DNS的IP地址判断用户的位置,筛选出距离用户较近的SLB,并把SLB的地址返回给本地DNS
  • 本地DNS将SLB的地址返回给浏览器,浏览器向其发起请求
  • SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器地址发回给浏览器
  • 浏览器根据返回的地址重定向
  • 如果缓存服务器有资源,使用;无,就向源服务器请求资源,把资源发回给浏览器

4. 加载解析主要资源角度

当我们去把服务端里的数据请求回来以后,就需要进行 HTML 解析,加载关键资源(JS、CSS),我们来看看 JS 是如何影响DOM生成的。正常来说JavaScript文件的下载是同步的,会阻塞DOM的解析;chrome 在这里做了很多优化,在这里会开启预解析操作,当渲染引擎接受到字节流以后,就会开启一个预解析线程去分析HTML中包含的JS、CSS 文件,当解析到相关文件以后,就会提前下载这些文件。

由于JavaScript线程会阻塞DOM,可采纳以下策略进行相关优化:

  • 使用 CDN 加速 JavaScript 文件的加载
  • 压缩 JavaScript 的文件体积
  • 若是 JavaScript 中不操作 DOM、CSS,可开启async、defer

上面的优化是从文件加载的角度来看的,下面是从JS文件执行的角度来优化。

5.主动交互角度

长任务:主动交互的角度是从 TTI 开始,就是主要是保证用户在做交互的时候要保证流畅,不要长时间的占用主线程(尽量保证任务的执行时间小于50ms)。

两种技术方案:

  • web-worker
  • Time Slicing(时间切片)

5.1 Web Worker

相当于是开启了一个新的线程,把需要循环的任务放在当中执行,这样就不会占用主线层,缺点是无法操作 DOM

const testWorker = new Worker('./worker.js')
setTimeout(_ => {
  testWorker.postMessage({})
  testWorker.onmessage = function (ev) {
    console.log(ev.data)
  }
}, 5000)

// worker.js
self.onmessage = function () {
  const start = performance.now()
  while (performance.now() - start < 1000) {}
  postMessage('done!')
}

5.2 Time Slicing

Time Slicing 就是把一个长任务切割成多个执行时间短的任务,
核心的实现是调用 setTimeout 会将任务添加到宏任务中、yield 的可以暂停执行

function block() {
    test(function *() {
        const start = performance.now();
        while(performance.now() - start < 1000) {
            console.log(222);
            yield;
        }
        console.log('done!');
    })
}

setTimeout(block, 5000);

function test(gen) {
    if(typeof gen === 'function') gen = gen();
    if(!gen || typeof gen.next !== 'function') return
    
    // 立即执行函数
    (function next() {
        // 调用next,拿到返回来的 {value: , done: }
        const rest = gen.next();
        // 结束
        if(res.done) return;
        setTimeout(next);
    })()
}

6. 数据读取

  • 变量的查询,从局部作用域到全局作用域的搜索过程越长速度越慢
  • 对象嵌套越深,读取速度越慢
  • 对象在原型链上藏得越深,读取也越慢
  • 数组元素和对象成员比较慢
  • 局部变量和 对象字面量的读取速度较快
  • 可对对象成员进行缓存到局部变量

6. DOM

  • 防止同时对 DOM 就行写 读操作
  • 操作DOM 先提升文档流,修改完毕以后,再复原。这样只需要重排重绘两次(脱离与回归)
  • 使用事件委托

8. 流程控制

  • 减少迭代的次数,防止时间复杂度到 O(n2)
  • 普通的循环迭代速度优于基于函数(for..of,forEach...)迭代(5~8倍)
  • for...in会遍历原型链上的属性,少用
  • Map代替 if...else 、switch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant