零、目录

image.png
如何进行性能优化?/谈一谈你知道的前端性能优化方案有哪些?/请描述提升页面性能的方式有哪些,如何进行首页加载优化?

零、性能优化

image.png

1.性能优化的原则

1 多使用内存,缓存或其他方法
2 减少CPU计算量,减少网络加载耗时
3 适用于所有编程的性能优化—-空间换时间

2.性能优化方向

(1) 让页面加载更快

  1. 减少资源体积,压缩代码,通常可以把代码压缩为原来的1/3左右
  2. 减少访问次数,合并代码,合并代码就能减少访问次数;
  3. 缓存;SSR服务器端渲染(服务端把页面和数据一块返回;前端拿到数据后直接展示)
  4. 使用更快的网络:CDN 根据区域去找服务器

(2)让页面渲染更快

  1. CSS放在head,js放在body最下面
  2. 尽早执行JS,用DOMContentLoaded触发
  3. 懒加载(图片懒还在,上滑加载更多)
  4. 对dom查询进行缓存
  5. 频繁dom操作,合并到一起插入dom结构
  6. 节流throttle 防抖debounce

(3)提升页面操作流畅度

  1. RAF requestAnimationFrame()
  2. 防抖节流

一、资源合并

image.png

二、http缓存

  1. 静态资源加hash后缀,根据文件内容计算hash
  2. 文件内容不变,则hash不变,则url不变
  3. url和文件不变,则会自动触发http缓存机制,返回304

image.png

三、CDN—-了解即可

image.png

1. CDN的概念

CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

典型的CDN系统由下面三个部分组成

  • 分发服务系统:**最基本的工作单元就是Cache设备,cache(边缘cache)负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户**。同时cache还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache设备的数量、规模、总服务能力是衡量一个CDN系统服务能力的最基本的指标。
  • 负载均衡系统:主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全局负载均衡(GSLB)和本地负载均衡(SLB)。全局负载均衡主要根据用户就近性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的cache的物理位置。本地负载均衡主要负责节点内部的设备负载均衡
  • 运营管理系统:运营管理系统分为运营管理和网络管理子系统负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。

2. CDN的作用

CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问
(1)在性能方面,引入CDN的作用在于:

  • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
  • 部分资源请求分配给了CDN,减少了服务器的负载

(2)在安全方面,CDN有助于防御DDoS、MITM等网络攻击

  • 针对DDoS:通过监控分析异常流量,限制其请求频率
  • 针对MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信

除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。

3. CDN的原理

CDN和DNS有着密不可分的联系,先来看一下DNS的解析域名过程,在浏览器输入 www.test.com 的解析过程如下:
(1) 检查浏览器缓存
(2)检查操作系统缓存,常见的如hosts文件
(3)检查路由器缓存
(4)如果前几步都没没找到,会向ISP(网络服务提供商)的LDNS服务器查询
(5)如果LDNS服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:

  • 根服务器返回顶级域名(TLD)服务器如.com,.cn,.org等的地址,该例子中会返回.com的地址
  • 接着向顶级域名服务器发送请求,然后会返回次级域名(SLD)服务器的地址,本例子会返回.test的地址
  • 接着向次级域名服务器发送请求,然后会返回通过域名查询到的目标IP,本例子会返回www.test.com的地址
  • Local DNS Server会缓存结果,并返回给用户,缓存在系统中

CDN的工作原理:
(1)用户未使用CDN缓存资源的过程:

  1. 浏览器通过DNS对域名进行解析(就是上面的DNS解析过程),依次得到此域名对应的IP地址
  2. 浏览器根据得到的IP地址,向域名的服务主机发送数据请求
  3. 服务器向浏览器返回响应数据

(2)用户使用CDN缓存资源的过程:

  1. 对于点击的数据的URL经过本地DNS系统的解析,发现该URL对应的是一个CDN专用的DNS服务器,DNS系统就会将域名解析权交给CNAME指向的CDN专用的DNS服务器
  2. CND专用DNS服务器将CND的全局负载均衡设备IP地址返回给用户
  3. 用户向CDN的全局负载均衡设备发起数据请求
  4. CDN的全局负载均衡设备根据用户的IP地址,以及用户请求的内容URL选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求
  5. 区域负载均衡设备选择一台合适的缓存服务器提供服务,将该缓存服务器的IP地址返回给全局负载均衡设备
  6. 全局负载均衡设备把服务器的IP地址返回给用户
  7. 用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端

如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源
六 【性能优化】 - 图6
CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的IP地址,或者该域名的一个CNAME,然后再根据这个CNAME来查找对应的IP地址。

4. CDN的使用场景

  • **使用第三方的CDN服务:**如果想要开源一些项目,可以使用第三方的CDN服务
  • **使用CDN进行静态资源的缓存:**将自己网站的静态资源放在CDN上,比如js、css、图片等。可以将整个项目放在CDN上,完成一键部署。
  • **直播传送直播本质上是使用流媒体进行传送,CDN也是支持流媒体传送的,所以直播完全可以使用CDN来提高访问速度**。CDN在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。

四、懒加载

(一)图片懒加载

1.实现图片懒加载的思路

判断图片所在位置是否在可视区内,图片移到可视区内进行加载,提供三种判断方法

  1. img.offsetTop < window.innerHeight + document.body.scrollTop
  2. element.getBoundingClientRect().top < clientHeight
  3. IntersectionObserver

1. 懒加载的概念

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。
如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。

2. 懒加载的特点

  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担
  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
  • 防止加载过多图片而影响其他资源文件的加载 :会影响网站应用的正常使用

3. 懒加载的实现原理

image.png
图片的加载是由src引起的,当对src赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5 的data-xxx属性来储存图片的路径,在需要加载图片的时候,将data-xxx中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载
注意:data-xxx 中的xxx可以自定义,这里我们使用data-src来定义。
懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。

使用原生JavaScript实现懒加载:
知识点:
(1)window.innerHeight 是浏览器可视区的高度
(2)document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离
(3)imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)
(4)图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;
图示:
image.png
代码实现:

  1. <div class="container">
  2. <img src="loading.gif" data-src="pic.png">
  3. <img src="loading.gif" data-src="pic.png">
  4. <img src="loading.gif" data-src="pic.png">
  5. <img src="loading.gif" data-src="pic.png">
  6. <img src="loading.gif" data-src="pic.png">
  7. <img src="loading.gif" data-src="pic.png">
  8. </div>
  9. <script>
  10. var imgs = document.querySelectorAll('img');
  11. function lozyLoad(){
  12. var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  13. var winHeight= window.innerHeight;
  14. for(var i=0;i < imgs.length;i++){
  15. if(imgs[i].offsetTop < scrollTop + winHeight ){
  16. imgs[i].src = imgs[i].getAttribute('data-src');
  17. }
  18. }
  19. }
  20. window.onscroll = lozyLoad();
  21. </script>

4. 懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。
  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

(二)路由懒加载

路由懒加载的三种方式:

1. 使用require + 箭头函数 动态加载

  • vue-router配置路由,使用vue的异步组件技术,可以实现按需加载。

但是,这种情况下一个组件生成一个js文件。

  1. {
  2. path: '/promisedemo',
  3. name: 'PromiseDemo',
  4. component: resolve => require(['../components/PromiseDemo'], resolve)
  5. }

2. 使用import + 箭头函数 动态加载

  1. {
  2. path: 'index',
  3. name: 'AdminIndex',
  4. component: () => import('@/views/admin/index'),
  5. },

3. es提案的import()

  • 推荐使用这种方式(需要webpack > 2.4);利用import()方法细颗粒度的分割代码;将每一个路由到的组件从主 bundle.js 文件分离出来了
  • webpack官方文档:webpack中使用import()

    1. vue官方文档:[路由懒加载(使用import())](https://link.segmentfault.com/?url=https%3A%2F%2Frouter.vuejs.org%2Fzh-cn%2Fadvanced%2Flazy-loading.html)
  • vue-router配置路由,代码如下:

    1. // 下面2行代码,没有指定webpackChunkName,每个组件打包成一个js文件。
    2. const ImportFuncDemo1 = () => import('../components/ImportFuncDemo1')
    3. const ImportFuncDemo2 = () => import('../components/ImportFuncDemo2')
    4. // 下面2行代码,指定了相同的webpackChunkName,会合并打包成一个js文件。
    5. // const ImportFuncDemo = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo')
    6. // const ImportFuncDemo2 = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo2')
    7. export default new Router({
    8. routes: [
    9. {
    10. path: '/importfuncdemo1',
    11. name: 'ImportFuncDemo1',
    12. component: ImportFuncDemo1
    13. },
    14. {
    15. path: '/importfuncdemo2',
    16. name: 'ImportFuncDemo2',
    17. component: ImportFuncDemo2
    18. }
    19. ]
    20. })

    4. webpack提供的require.ensure()

  • vue-router配置路由,使用webpack的require.ensure技术,也可以实现按需加载。

这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
举例如下:

  1. {
  2. path: '/promisedemo',
  3. name: 'PromiseDemo',
  4. component: resolve => require.ensure([], () => resolve(require('../components/PromiseDemo')), 'demo')
  5. },
  6. {
  7. path: '/hello',
  8. name: 'Hello',
  9. // component: Hello
  10. component: resolve => require.ensure([], () => resolve(require('../components/Hello')), 'demo')
  11. }

image.png

require和import只是模块化的规范不一样,import是es6 ,require是COMMONJS,如果再加个规范的话,他懒加载的形式还能再多一种

五、回流与重绘

1. 回流与重绘的概念及触发条件

(1)回流(重排)

当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流。

下面这些操作会导致回流

  • 页面的首次渲染
  • 浏览器的窗口大小发生变化
  • 元素的内容发生变化
  • 元素的尺寸或者位置发生变化
  • 元素的字体大小发生变化
  • 激活CSS伪类
  • 查询某些属性或者调用某些方法
  • 添加或者删除可见的DOM元素

在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:

  • 全局范围:从根节点开始,对整个渲染树进行重新布局
  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局

(2)重绘

当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。

下面这些操作会导致重绘

  • color、background 相关属性:background-color、background-image 等
  • outline 相关属性:outline-color、outline-width 、text-decoration
  • border-radius、visibility、box-shadow

注意: 当触发回流(重排)时,一定会触发重绘,但是重绘不一定会引发回流

2. 如何避免回流与重绘?

减少回流与重绘的措施

  • 操作DOM时,尽量在低层级的DOM节点进行操作
  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局
  • 使用CSS的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
  • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 将DOM的多个读操作(或者写操作放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制。

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列
浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。

3. 如何优化动画?

对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作DOM,就就会导致页面的性能问题,我们可以将动画的position属性设置为absolute或者fixed,将动画脱离文档流,这样他的回流就不会影响到页面了

4. documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?

MDN中对documentFragment的解释:
DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作DOM相比,将DocumentFragment 节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。

六、节流与防抖

1. 对节流与防抖的理解

  1. 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求

防抖的应用场景:

  1. - **按钮**提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
  2. - 服务端验证场景:**表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次**,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce
  1. 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。

节流的应⽤场景:

  1. - 拖拽场景:固定时间内只执⾏⼀次,**防⽌超⾼频次触发位置变动**
  2. - 缩放场景:**监控浏览器resize**
  3. - 动画场景:**避免短时间内多次触发动画引起性能问题**

2. 手写节流函数和防抖函数

节流防抖链接
函数防抖的实现:
每次触发事件将重新计时避免因为用户的多次点击向后端发送多次请求

  1. <body>
  2. <input id="input1"></input>
  3. <script>
  4. var input1 = document.getElementById('input1')
  5. function debounce(fn, waitTime) {
  6. var timer = null // 闭包,仅该方法可以访问该值
  7. return function (){
  8. var content = this
  9. args = [...arguments]
  10. if (timer) {
  11. // 如果用户触发了定时器但是还没到500ms就又触发了事件;这个时候定时器打开的,需要清理掉
  12. clearTimeout(timer)// 先清掉定时器,timer是个标记定时器的变量
  13. timer = null // 再释放空间
  14. }
  15. // 设置定时器,使事件 间隔指定事件后 执行
  16. timer = setTimeout(() => {
  17. fn.apply(content, args)
  18. }, waitTime)
  19. }
  20. }
  21. input1.addEventListener('keyup',debounce(()=>{console.log('防抖实现!')},500))
  22. </script>
  23. </body>

函数节流的实现:
当频繁操作的时候,保持一定的频率触发;固定每多少秒触发一次;每次触发对定时器无影响
image.png

  1. // 时间戳版
  2. function throttle(fn, delay) {
  3. var preTime = Date.now();
  4. return function() {
  5. var context = this,
  6. args = [...arguments],
  7. nowTime = Date.now();
  8. // 如果两次时间间隔超过了指定时间,则执行函数。
  9. if (nowTime - preTime >= delay) {
  10. preTime = Date.now();
  11. return fn.apply(context, args);
  12. }
  13. };
  14. }
  15. // 定时器版
  16. <body>
  17. <input id="input1"></input>
  18. <script>
  19. var input1 = document.getElementById('input1')
  20. function throttle(fn, waitTime) {
  21. let timer1 = null
  22. return function(){
  23. var content = this
  24. let args = [...arguments]
  25. if (!timer1){ // 只要没有定时器,就开启一个定时器, 有定时器则不处理 保持一定的频率触发
  26. timer1 = setTimeout(() => {
  27. fn.apply(content, args)
  28. clearTimeout(timer1) // 记得关定时器
  29. timer1 = null // 释放内存
  30. }, waitTime);
  31. }
  32. }
  33. }
  34. input1.addEventListener('keyup',throttle(()=>{console.log('实现了没有呀!')},500))
  35. </script>
  36. </body>

image.png

七、Webpack优化

1. 如何提⾼webpack的打包速度?

(1)优化 Loader

对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,这是可以优化的。
首先我们优化 Loader 的文件搜索范围

  1. module.exports = {
  2. module: {
  3. rules: [
  4. {
  5. test: /\.js$/, // js 文件才使用 babel
  6. loader: 'babel-loader',
  7. include: [resolve('src')],// 只在 src 文件夹下查找
  8. exclude: /node_modules/ //输出文件
  9. }
  10. ]
  11. }
  12. }

对于 Babel 来说,希望只作用在 JS 代码上的,然后 node_modules 中使用的代码都是编译过的,所以完全没有必要再去处理一遍。
当然这样做还不够,还可以将 Babel 编译过的文件缓存起来下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间

  1. loader: 'babel-loader?cacheDirectory=true'

(2)HappyPack

受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了

  1. // npm i -D happypack
  2. module: {
  3. loaders: [
  4. {
  5. test: /\.js$/,
  6. include: [resolve('src')],
  7. exclude: /node_modules/,
  8. loader: 'happypack/loader?id=happybabel' // id 后面的内容
  9. }
  10. ]
  11. },
  12. plugins: [
  13. new HappyPack({
  14. id: 'happybabel',
  15. loaders: ['babel-loader?cacheDirectory'],
  16. threads: 4 // 开启 4 个线程
  17. })
  18. ]

HappyPack 对file-loader、url-loader 支持的不友好,所以不建议对该loader使用。

(3)DllPlugin

DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin的使用方法如下:

  1. // 单独配置在一个文件中
  2. // webpack.dll.conf.js
  3. const path = require('path')
  4. const webpack = require('webpack')
  5. module.exports = {
  6. entry: {
  7. vendor: ['react']// 想统一打包的类库
  8. },
  9. output: {
  10. path: path.join(__dirname, 'dist'), // 输出路径
  11. filename: '[name].dll.js', // 输出文件名
  12. library: '[name]-[hash]' // hash
  13. },
  14. plugins: [
  15. new webpack.DllPlugin({
  16. name: '[name]-[hash]',// name 必须和 output.library 一致
  17. context: __dirname, // 该属性需要与 DllReferencePlugin 中一致
  18. path: path.join(__dirname, 'dist', '[name]-manifest.json')
  19. })
  20. ]
  21. }

然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin 将依赖文件引入项目中

  1. // webpack.conf.js
  2. module.exports = {
  3. // ...省略其他配置
  4. plugins: [
  5. new webpack.DllReferencePlugin({
  6. context: __dirname,
  7. manifest: require('./dist/vendor-manifest.json'),// manifest 就是之前打包出来的 json 文件
  8. })
  9. ]
  10. }

(4)代码压缩

在 Webpack3 中,一般使用 UglifyJS 来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS,从而提高效率

在 Webpack4 中,不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能

(5)其他

可以通过一些小的优化点来加快打包速度

  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 [‘.js’, ‘.json’],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助

2. 如何减少 Webpack 打包体积

(1)按需加载

在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 loadash 这种大型类库同样可以使用这个功能。
按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。

(2)Scope Hoisting

Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去
比如希望打包两个文件:

  1. // test.js
  2. export const a = 1
  3. // index.js
  4. import { a } from './test.js'

对于这种情况,打包出来的代码会类似这样:

  1. [
  2. /* 0 */
  3. function (module, exports, require) {
  4. //...
  5. },
  6. /* 1 */
  7. function (module, exports, require) {
  8. //...
  9. }
  10. ]

但是如果使用 Scope Hoisting ,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码:

  1. [
  2. /* 0 */
  3. function (module, exports, require) {
  4. //...
  5. }
  6. ]

这样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules 就可以了:

  1. module.exports = {
  2. optimization: {
  3. concatenateModules: true
  4. }
  5. }

(3)Tree Shaking

Tree Shaking 可以实现删除项目中未被引用的代码,比如:

  1. // test.js
  2. export const a = 1
  3. export const b = 2
  4. // index.js
  5. import { a } from './test.js'

对于以上情况,test 文件中的变量 b 如果没有在项目中使用到的话,就不会被打包到文件中。
如果使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。

3. 如何⽤webpack来优化前端性能?

⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css
  • 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径
  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 —optimize-minimize 来实现
  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

    4. 如何提⾼webpack的构建速度?

  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常⽤库
  3. 利⽤ DllPluginDllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使⽤ Happypack 实现多线程加速编译
  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
  6. 使⽤ Tree-shakingScope Hoisting 剔除多余代码

八、SSR服务器渲染;减少网络请求

image.png

九、DOM优化

缓存查询

减少dom的查询
image.png

多个DOM的操作

image.png

十、尽早执行js

image.png

十一、RAF requestAnimationFrame(提高页面流畅度)

image.png
要求:
image.png
可以这样写,这样写时间是自己控制的,原始写法
image.png
但如果想每次变化的更多的话,每次变化的时间差就要更多;看着也会卡顿一些,这样的话,其实我们时间需要计算的
image.png
image.png

十二、浏览器性能优化

1.Perfomance API

image.pngimage.png

  1. 输入了url之后,因为之前已经有一个网站了,要先把之前的内容清空;再开始请求/导航(navigationStart);
  2. 然后可能重定向寻找到服务器(redirectStart,redirectEnd);
  3. 开始发送请求(fetchStart);
  4. 有缓存就用缓存,没缓存就解析(APP cache)
  5. DNS解析(DNS解析的时长就是domainLookupStart的时间-domainLookupEnd的时间)
  6. TCP三次握手
  7. 请发送求(主要耗时)
  8. 响应(主要耗时)
  9. 加载DOM DOM交互绑定事件/DOM的加载完成

image.png
image.pngimage.png
image.png
image.png

2.网络优化策略

  • 减少HTTP请求,合并js css ;合理内嵌css,JS
  • 设置缓存(强制缓存,对比缓存304)
  • 避免重定向,重定向会降低响应速度(301 302)
  • dns-prefetch DNS预解析
  • 域名分片,将资源放到不同的域名下,接触同一个域名最多处理6个TCP链接问题;
  • 采用CDN加速加快访问速度,最近指派,高度可用
  • gzip压缩优化(html js css;图片不要压缩了本来就压缩了,再去压缩会变得更大)
  • 加载数据优先级:preload(预先请求当前页面需要的资源) prefetch(将来页面中使用的资源,子路由,当前页面在空闲的时候加载未来要用的资源) 将数据缓存到HTTP缓存中,之后使用的时候直接取

最好首页的内容都用preload 子页面的内容用prefetch

  1. <link rel="preload" href="style.css" as="style">
  2. as->优先级很高

3.关键渲染路径

(一)关键渲染路径的定义

关键渲染路径:只要操作渲染,就一定会经历这些步骤:重绘和重排不一定要经历
image.png
重排(回流)reflow:重新布局;添加元素,删除元素,修改大小,移动元素位置,获取位置相关信息(因为要去先去重排才能获取位置..所以也会导致重排);性能更差一些
重绘repaint:重新绘制;页面中元素样式的改变并不影响他在文档流中的位置

(二)强制同步布局

强制同步布局问题:js强制将计算样式和布局操作提前到当前的任务中,会导致耗时加大
读和写要分开写,这样才不会导致强制xxx布局,读和写放一起写的话 会不停的触发布局:
image.pngimage.png

解决方法: 把获取位置相关信息的变量缓存起来,等需要使用的时候再去用

(二)布 局抖动布局

布局抖动(layout thrashing)问题:在js中反复执行布局操作,就是布局抖动

(三)减少回流和重绘

  • 脱离文档流:脱离了就不会影响其他人的布局了
  • 渲染时给图片增加固定宽高:加载前加载后大小不一样的话会影响其他元素的布局
  • 尽量使用css3动画:css3动画不影响布局,动画效果都是采用GPU加速还有图层的复合;没有经历重流和重绘
  • 产生图层树,脱离文档流单独的一个图层,就不会影响其他元素;给标签单独加上样式:will-change:transform 可以让浏览器单独给它加一个图层,浏览器就知道他后续可能发生变化

产生图层树的有:定位属性,透明属性,transform属性,clip属性等等

十三、静态文件优化

1.图片优化

(一) 图片优化?

  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替
  2. 减少图片尺寸,节约用户流量
  3. img标签设置alt属性,提升图片加载失败时的用户体验,如果图片加载不出来可以提示图片内容丢失
  4. 图片懒加载,原生的loading:lazy+宽高;但是这个属性无法控制什么时候懒加载,所以我们不用这个属性
  5. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  6. 小图使用 base64URL 减少图片请求,如果图片很大的话用base64的话图片会变得更大
  7. 大图可以使用 渐进式图片
  8. 不要使用空链接(src为空的标签)的图
  9. 不同环境下加载不同尺寸和像素的图片 ``` 六 【性能优化】 - 图31

根据不同的屏幕宽度设置不同的图片大小:宽度是500像素的时候显示 100px的图片;宽度是600像素的时候显示200px的图片

  1. 10. 将多个图标文件整合到一张图片中(**雪碧图**)
  2. 10. **选择正确的图片格式**:
  3. - 对于能够显示 WebP 格式的浏览器尽量使用 **WebP** 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来**更小的图片体积**,而且拥有肉眼识别**无差异的图像质量**,缺点就是兼容性并不好
  4. - **小图使用 PNG**,其实对于大部分**图标**这类图片,完全可以使用 **SVG** 代替
  5. - 照片使用 **JPEG**
  6. <a name="sbCnM"></a>
  7. ### (二)常见的图片格式及使用场景
  8. 1. JPG:适合色彩丰富的图片,banner图,不合适图形文字,图标(纹理有锯齿),不支持透明度
  9. 1. PNG:适合纯色 透明 图标 支持半透明; 不合适色彩丰富的图片,因为无损存储会导致存储体积大
  10. 1. GIF:适合动画,可以动的图标;不支持半透明,不适合存储彩色图片
  11. 1. WebP:适合半透明,可保证图片质量和较小的体积
  12. 1. svg:矢量图,相对于jpgpng体积更小,渲染成本高,适合小且色彩单一的图标
  13. (再来一遍?)<br />(1)**BMP**,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以BMP格式的图片通常是较大的文件。<br />(2)**GIF**是无损的、采用索引色的点阵图。采用LZW压缩算法进行编码。文件小,是GIF格式的优点,同时,GIF格式还具有支持动画以及透明的优点。但是GIF格式仅支持8bit的索引色,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景。<br />(3)**JPEG**是有损的、采用直接色的点阵图。JPEG的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储**照片**,与GIF相比,JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF更大。<br />(4)**PNG-8**是无损的、使用索引色的点阵图。PNG是一种比较新的图片格式,PNG-8是非常好的**GIF格式替代者**,在可能的情况下,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下,PNG-8具有更小的文件体积。除此之外,PNG-8还支持透明度的调节,而GIF并不支持。除非需要动画的支持,否则没有理由使用GIF而不是PNG-8。<br />(5)**PNG-24**是无损的、使用直接色的点阵图。PNG-24的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24格式的文件大小要比BMP**小**得多。当然,PNG24的图片还是要比JPEGGIFPNG-8大得多。<br />(6)**SVG**是无损的矢量图。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成。当放大SVG图片时,看到的还是线和曲线,而不会出现像素点。这意味着SVG图片在放大时,不会失真,所以它非常适合用来绘制**LogoIcon**等。<br />(7)**WebP**是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢?就是说**相同质量的图片,WebP具有更小的文件体积**。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有Chrome浏览器和Opera浏览器支持WebP格式,**_兼容性不太好_**。
  14. - 在无损压缩的情况下,相同质量的WebP图片,文件大小要比PNG26%;
  15. - 在有损压缩的情况下,具有相同图片精度的WebP图片,文件大小要比JPEG25%~34%;
  16. - WebP图片格式支持图片透明度,一个无损压缩的WebP图片,如果要支持透明度只需要22%的格外文件大小。
  17. <a name="dxRwC"></a>
  18. ## 2.html优化
  19. 1. 语义化html,代码简洁清晰,利于搜索引擎,便于团队开发
  20. 1. 声明字符编码,让浏览器快速确定如何渲染网页内容
  21. 1. 减少html嵌套关系,减少dom节点数量
  22. 1. 删除多余空行和空格,注释和无用属性等
  23. 1. html减少 iframes的使用(iframe会阻塞onload事件,可以动态加载iframe
  24. 1. 避免用table布局
  25. <a name="MvTKl"></a>
  26. ## 3.css优化
  27. 1. 减少伪类选择器,减少样式层数,减少使用通配符
  28. 1. 避免使用css表达式,css表达式会频繁求值,当滚动页面或者鼠标移动的时候会重新计算
  29. 1. 删除空行,注释,减少无意义的单位,css进行压缩
  30. 1. 使用外链的css,可以对css进行缓存
  31. 1. 添加媒体字段,只加载有效的css文件
  32. 1. css contain属性将元素进行隔离
  33. 1. 减少@import使用,因为@import采用的是串行加载(link可以并行加载)
  34. <a name="uTB8s"></a>
  35. ## 4.js优化
  36. 1. js会阻碍html的加载,通过asyncdefer异步加载文件
  37. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634186532462-0e3657ef-0bba-4bb6-bc0e-237b6dc90059.png#clientId=u55f7a833-141f-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=136&id=ua8dabdc4&margin=%5Bobject%20Object%5D&name=image.png&originHeight=310&originWidth=1664&originalType=binary&ratio=1&rotation=0&showTitle=false&size=254004&status=done&style=none&taskId=u55230c88-8195-4d11-8a45-ee761be3955&title=&width=729)<br />async,defer都有异步的功能,async表示的是代码下载完了就立刻执行js,会阻碍dom解析;defer表示的是dom加载完后再执行js
  38. 2. 减少dom操作,缓存访问过的元素
  39. 2. 操作不要直接应用到DOM上,尽量用到虚拟DOM上,最后一次性应用到DOM
  40. 2. 使用 webworker 解决程序阻塞问题 非常大的值做复杂计算可以用
  41. 2. IntersectionObserver 滚动api 懒加载的一种
  42. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634187618465-58ebb0db-8981-43e9-b522-9041f025ae29.png#clientId=u55f7a833-141f-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=315&id=u46573c2d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1260&originWidth=2128&originalType=binary&ratio=1&rotation=0&showTitle=false&size=2398407&status=done&style=none&taskId=uf6731e24-4171-4fd1-94ab-7d543a49e76&title=&width=532)
  43. 6. 虚拟滚动/虚拟列表 针对的场景是长列表 比如列表数据10万条,如果你的应用存在非常长的或者无限滚动的列表,那么需要采用窗口化的技术来优化性能,只需要渲染少部分的内容(可视区域),减少重新渲染组件和创建dom节点的时间;vertual-scroll-list 懒加载的一种;虚拟滚动的原理就是在滚动的过程一边显示新的元素 一边移除旧的元素,还要保证滚动过程中一边显示新的元素一遍移除旧的元素而滚动不卡顿;

当前有一个视口,这个视口内有很多元素,我们想知道元素一共有度高,假如一个元素5像素,一共100个;则高度为5*100;那么我们可以在视口内拿一个500高的div把它撑开,这个div里并没有任何数据,我们可以根据计算滚动条的距离,把当前需要显示的列表提取出来显示,剩下的不显示;

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634215633876-5583b913-a7ca-4b81-a231-d1c6d7331b74.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=484&id=u9985bbe2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1440&originWidth=1786&originalType=binary&ratio=1&rotation=0&showTitle=false&size=537099&status=done&style=none&taskId=u2ff162ad-2651-44e6-91d9-2bfd7806c36&title=&width=600)
  2. 7. requestAnimationFramerequestIdleCallback
  3. 我们浏览器的刷新频率在60FPS/60帧,每秒刷新频率60次,1s=1000ms1000/60 = 16.6 ;所以每一帧是16.6ms;每一帧做了哪些事情?<br />最后面两个是,当我们的每一帧差不多走完的时候,如果还有空余时间可以用来做requestIdleCallback 的事情<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634216775900-1137583a-af04-4973-a1cf-930586a45230.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=220&id=nRyla&margin=%5Bobject%20Object%5D&name=image.png&originHeight=440&originWidth=1812&originalType=binary&ratio=1&rotation=0&showTitle=false&size=370931&status=done&style=none&taskId=u0c4322cb-4a5a-4f56-9ef0-f79c61b02bf&title=&width=906)<br />(1)定时器模拟动画效果(不稳定)
  4. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634216114332-05e17b4f-13b1-406c-9d90-804d72f84135.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=124&id=cRodr&margin=%5Bobject%20Object%5D&name=image.png&originHeight=496&originWidth=1422&originalType=binary&ratio=1&rotation=0&showTitle=false&size=487618&status=done&style=none&taskId=u3e824393-ad0b-41df-8105-163903b07d9&title=&width=356)![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634216054189-b88e3d33-0230-4a3d-8240-5c9b5d885bda.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=206&id=cfF5X&margin=%5Bobject%20Object%5D&name=image.png&originHeight=822&originWidth=2148&originalType=binary&ratio=1&rotation=0&showTitle=false&size=582284&status=done&style=none&taskId=ua7542a5b-7aae-4ece-8452-7dfb4b74c48&title=&width=537)
  5. 2RAF<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634216536919-c7d05fd1-1ca7-4af0-9f4d-709b70a31081.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=187&id=vJrsW&margin=%5Bobject%20Object%5D&name=image.png&originHeight=746&originWidth=1938&originalType=binary&ratio=1&rotation=0&showTitle=false&size=750006&status=done&style=none&taskId=u9e4d3ada-8273-4800-a02e-b211410fef0&title=&width=485)![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634217119217-5de8cc2a-7cdb-46aa-8b06-15ca0f53c054.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=171&id=S3aEw&margin=%5Bobject%20Object%5D&name=image.png&originHeight=684&originWidth=1864&originalType=binary&ratio=1&rotation=0&showTitle=false&size=613706&status=done&style=none&taskId=u393c14de-4eb2-4543-b19f-4c33013d434&title=&width=466)<br />(2)requestIdleCallback<br />兼容性非常差;在执行这三个任务的时候把这三个事件匀到不同的帧中;把任务均匀的分到了每一帧去;这样可以把浏览器的剩余空间使用了,而不是把全部的脚本执行完再去动画<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634218117483-58f30abf-3348-4a09-8683-905ca6fa29c1.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=309&id=BVjNr&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1236&originWidth=1752&originalType=binary&ratio=1&rotation=0&showTitle=false&size=1226657&status=done&style=none&taskId=ua8360f0e-fba1-46a5-b879-0aa8609f31d&title=&width=438)![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634218204803-2eaa7842-0024-4faa-a54a-df15bc8519d7.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=200&id=av5nF&margin=%5Bobject%20Object%5D&name=image.png&originHeight=800&originWidth=1872&originalType=binary&ratio=1&rotation=0&showTitle=false&size=593491&status=done&style=none&taskId=ucf43924f-69dc-41e0-9c64-7e54ce6a4fb&title=&width=468)
  6. 8. 尽量避免使用 eval,消耗时间久:用eval 的性能比用function的差
  7. 8. 使用事件委托,减少事件绑定个数
  8. 8. 尽量使用canvas动画,css动画
  9. <a name="Qi6F6"></a>
  10. ## 5. 字体优化
  11. 有时候引入中文字体包的话,包会很大,可以做一下以下优化<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634256017017-ee380c7f-6a87-4f53-9a23-f421ff866b3f.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=544&id=u84815a64&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1088&originWidth=1508&originalType=binary&ratio=1&rotation=0&showTitle=false&size=753938&status=done&style=none&taskId=uea62fba1-bf28-467a-881f-3e046607168&title=&width=754)
  12. <a name="vvzKE"></a>
  13. ## 6. 优化策略
  14. - 关键资源个数越多,首次页面加载时间会越长
  15. - 关键资源大小,内容越小,下载时间越短
  16. - 优化白屏:内联js和内联css,就可减少资源个数和资源大小,移除文件下载
  17. - 预渲染:打包的时候进行预渲染
  18. - 使用SSR加速首屏(耗费服务器资源),有利于SEO优化,首屏利用服务端渲染,后续交互采用客户端渲染
  19. <a name="nk1qo"></a>
  20. ## 7. 浏览器存储
  21. cookie<br />给静态资源和普通获取资源的请求资源路径采用不同的域名,是为了静态资源访问时可以不携带cookie,还有一个就是静态资源用CDN加速。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634257519920-4b005646-0653-418a-ac09-8a7ea4a13ab0.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=208&id=ubeceba91&margin=%5Bobject%20Object%5D&name=image.png&originHeight=416&originWidth=1482&originalType=binary&ratio=1&rotation=0&showTitle=false&size=547423&status=done&style=none&taskId=ud7b7ab7a-08a9-460b-a913-1971877e252&title=&width=741)<br />localstorage<br />除了存一般的数据,还可以存html js 文件 :比如我们的百度搜索页就是把所有的资源存到了本地local中,就是以下的写法<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634257567170-026da015-0a18-4397-bdd0-ae67376d1d2c.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=545&id=u0dfc81da&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1090&originWidth=1452&originalType=binary&ratio=1&rotation=0&showTitle=false&size=909181&status=done&style=none&taskId=u0101e0d5-1eb1-4548-b755-de4ad4e3a70&title=&width=726)<br />sessionStorage:会话级别,页面间传值<br />indexDB:浏览器本地数据库(基本无上限)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634257912395-56b06d79-e0bd-43b5-ab1b-e7eb7415e930.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=435&id=ueb423ddc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=870&originWidth=1328&originalType=binary&ratio=1&rotation=0&showTitle=false&size=948496&status=done&style=none&taskId=u892adc45-dad1-42ac-a6fe-59872f7e40d&title=&width=664)
  22. <a name="Y45yS"></a>
  23. ## 8. 增加体验 PWA(progressive web app)
  24. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634257964135-4b882c72-6a18-4e33-b520-6b3324c8dd3d.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=246&id=u787bc932&margin=%5Bobject%20Object%5D&name=image.png&originHeight=492&originWidth=1788&originalType=binary&ratio=1&rotation=0&showTitle=false&size=642168&status=done&style=none&taskId=u3a8de989-5f4a-4fff-a312-fe879b60aea&title=&width=894)
  25. <a name="CsPwh"></a>
  26. ## 9. LightHouse使用
  27. 性能测试表:<br />性能指标 可访问性 最大事件 SEO <br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634266825079-0f3aeae3-1d2c-460f-9a7c-e8592b0c215c.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=84&id=u892da1f2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=168&originWidth=1194&originalType=binary&ratio=1&rotation=0&showTitle=false&size=115394&status=done&style=none&taskId=u9cc3aacf-f6f2-403f-9425-5d3560904f9&title=&width=597)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22056270/1634266919150-aeeb7e5f-a56f-40ba-a052-79cd71c60f44.png#clientId=ubd160ae8-6d6c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=413&id=u02409d7d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=825&originWidth=1399&originalType=binary&ratio=1&rotation=0&showTitle=false&size=455386&status=done&style=none&taskId=uaf191b61-0441-44ee-b81c-856adc90d05&title=&width=699.5)
  28. <a name="kwbVi"></a>
  29. # 十四. 掘金小册
  30. <a name="cThsa"></a>
  31. ## 1.开篇
  32. 性能优化是什么、为什么、怎么做?<br /> 20% 的理论,加上至少 80% 的实践
  33. 从**网络层面**和**渲染层面**两个大的维度来逐个点亮前端性能优化的技能树:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22056270/1642741665531-c3cf59e8-602a-4c1e-ae4f-9a22102d1641.png#clientId=uf7f75afd-da3e-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=270&id=ued9c378a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=270&originWidth=490&originalType=binary&ratio=1&rotation=0&showTitle=false&size=52788&status=done&style=none&taskId=ub8afbc92-091f-41ec-9d1a-3e570687c62&title=&width=490)<br />具体来说,DNS 解析花时间,能不能尽量减少解析次数或者把解析前置?能——浏览器 DNS 缓存和 DNS prefetch。<br />TCP 每次的三次握手都急死人,有没有解决方案?有——长连接、预连接、接入 SPDY 协议。如果说这两个过程的优化往往需要我们和团队的服务端工程师协作完成,前端单方面可以做的努力有限,<br />那么 HTTP 请求呢?——在减少请求次数和减小请求体积方面,我们应该是专家!<br />再者,服务器越远,一次请求就越慢,那部署时就把静态资源放在离我们更近的 CDN 上是不是就能更快一些?
  34. 网络层面的性能优化。<br />再往下走就是浏览器端的性能优化——这部分涉及资源加载优化、服务端渲染、浏览器缓存机制的利用、DOM 树的构建、网页排版和渲染过程、回流与重绘的考量、DOM 操作的合理规避等等——这正是前端工程师可以真正一展拳脚的地方。学习这些知识,不仅可以帮助我们从根本上提升页面性能,更能够大大加深个人对浏览器底层原理、运行机制的理解,一举两得!<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22056270/1642741741247-36bfa273-c051-48f3-8a07-4b19e931c959.png#clientId=uf7f75afd-da3e-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=1506&id=u7cddfae8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1506&originWidth=2478&originalType=binary&ratio=1&rotation=0&showTitle=false&size=799622&status=done&style=none&taskId=ue4f20586-9b3e-4ea1-9a59-081ebb73b04&title=&width=2478)
  35. <a name="CMT01"></a>
  36. ## 2.webpack 性能调优与 Gzip 原理
  37. <a name="MERTk"></a>
  38. ### (一)webpack 优化方案
  39. <a name="y5dIT"></a>
  40. #### (1)构建过程提速策略:不要让 loader 做太多事情
  41. babel-loader 为例:babel-loader 无疑是强大的,但它也是慢的。<br />最常见的优化方式是,用 include exclude 来帮我们避免不必要的转译,<br />比如 webpack 官方在介绍 babel-loader 时给出的示例:
  42. ```json
  43. module: {
  44. rules: [
  45. {
  46. test: /\.js$/,
  47. exclude: /(node_modules|bower_components)/, //规避了对庞大的 node_modules 文件夹或者 bower_components 文件夹的处理
  48. use: {
  49. loader: 'babel-loader',
  50. options: {
  51. presets: ['@babel/preset-env']
  52. }
  53. }
  54. }
  55. ]
  56. }

这段代码帮我们规避了对庞大的 node_modules 文件夹或者 bower_components 文件夹的处理。但通过限定文件范围带来的性能提升是有限的。
除此之外,如果我们选择开启缓存将转译结果缓存至文件系统,则至少可以将 babel-loader 的工作效率提升两倍。要做到这点,我们只需要为 loader 增加相应的参数设定:

  1. loader: 'babel-loader?cacheDirectory=true'

尽管我们可以在 loader 配置时通过写入 exclude 去避免 babel-loader 对不必要的文件的处理,但是考虑到这个规则仅作用于这个 loader,像一些类似 UglifyJsPlugin 的 webpack 插件在工作时依然会被这些庞大的第三方库拖累,webpack 构建速度依然会因此大打折扣。所以针对这些庞大的第三方库,我们还需要做一些额外的努力。

(2)不要放过第三方库

第三方库以 node_modules 为代表,它们庞大得可怕,却又不可或缺
处理第三方库的姿势有很多,其中,Externals 不够聪明,一些情况下会引发重复打包的问题;而 CommonsChunkPlugin 每次构建时都会重新构建一次 vendor;出于对效率的考虑,我们这里为大家推荐 DllPlugin

DllPlugin 是基于 Windows 动态链接库(dll)的思想被创作出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。这个依赖库不会跟着你的业务代码一起被重新打包,只有当依赖自身发生版本变化时才会重新打包

用 DllPlugin 处理文件,要分两步走:

  • 基于 dll 专属的配置文件,打包 dll 库
  • 基于 webpack.config.js 文件,打包业务代码

以一个基于 React 的简单项目为例,我们的 dll 的配置文件 可以编写如下:

  1. const path = require('path')
  2. const webpack = require('webpack')
  3. module.exports = {
  4. entry: {
  5. // 依赖的库数组
  6. vendor: [
  7. 'prop-types',
  8. 'babel-polyfill',
  9. 'react',
  10. 'react-dom',
  11. 'react-router-dom',
  12. ]
  13. },
  14. output: {
  15. path: path.join(__dirname, 'dist'),
  16. filename: '[name].js',
  17. library: '[name]_[hash]',// libary在这
  18. },
  19. plugins: [
  20. new webpack.DllPlugin({
  21. // DllPluginname属性需要和libary保持一致
  22. name: '[name]_[hash]',
  23. path: path.join(__dirname, 'dist', '[name]-manifest.json'),
  24. // context需要和webpack.config.js保持一致
  25. context: __dirname,
  26. }),
  27. ],
  28. }

编写完成之后,运行这个配置文件,我们的 dist 文件夹里会出现这样两个文件:

  1. vendor-manifest.json
  2. vendor.js

vendor.js 不必解释,是我们第三方库打包的结果。这个多出来的 vendor-manifest.json,则用于描述每个第三方库对应的具体路径,我这里截取一部分给大家看下:

  1. {
  2. "name": "vendor_397f9e25e49947b8675d",
  3. "content": {
  4. "./node_modules/core-js/modules/_export.js": {
  5. "id": 0,
  6. "buildMeta": {
  7. "providedExports": true
  8. }
  9. },
  10. "./node_modules/prop-types/index.js": {
  11. "id": 1,
  12. "buildMeta": {
  13. "providedExports": true
  14. }
  15. },
  16. ...
  17. }
  18. }

随后,我们只需在 webpack.config.js 里针对 dll 稍作配置:

  1. const path = require('path');
  2. const webpack = require('webpack')
  3. module.exports = {
  4. mode: 'production',
  5. // 编译入口
  6. entry: {
  7. main: './src/index.js'
  8. },
  9. // 目标文件
  10. output: {
  11. path: path.join(__dirname, 'dist/'),
  12. filename: '[name].js'
  13. },
  14. // dll相关配置
  15. plugins: [
  16. new webpack.DllReferencePlugin({
  17. context: __dirname,
  18. // manifest就是我们第一步中打包出来的json文件
  19. manifest: require('./dist/vendor-manifest.json'),
  20. })
  21. ]
  22. }

一次基于 dll 的 webpack 构建过程优化,便大功告成了!

(3)Happypack——将 loader 由单进程转为多进程

webpack 是单线程的,就算此刻存在多个任务,你也只能排队一个接一个地等待处理。这是 webpack 的缺点,好在我们的 CPU 是多核的,Happypack 会充分释放 CPU 在多核并发方面的优势,帮我们把任务分解给多个子进程去并发执行,大大提升打包效率。
HappyPack 的使用方法也非常简单,只需要我们把对 loader 的配置转移到 HappyPack 中去就好,我们可以手动告诉 HappyPack 我们需要多少个并发的进程:

  1. const HappyPack = require('happypack')
  2. // 手动创建进程池
  3. const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
  4. module.exports = {
  5. module: {
  6. rules: [
  7. ...
  8. {
  9. test: /\.js$/,
  10. // 问号后面的查询参数指定了处理这类文件的HappyPack实例的名字
  11. loader: 'happypack/loader?id=happyBabel',
  12. ...
  13. },
  14. ],
  15. },
  16. plugins: [
  17. ...
  18. new HappyPack({
  19. // 这个HappyPack的“名字”就叫做happyBabel,和楼上的查询参数遥相呼应
  20. id: 'happyBabel',
  21. // 指定进程池
  22. threadPool: happyThreadPool,
  23. loaders: ['babel-loader?cacheDirectory']
  24. })
  25. ],
  26. }

(4)构建结果体积压缩

文件结构可视化,找出导致体积过大的原因
这里为大家介绍一个非常好用的包组成可视化工具——webpack-bundle-analyzer,配置方法和普通的 plugin 无异,它会以矩形树图的形式将包内各个模块的大小和依赖关系呈现出来,格局如官方所提供这张图所示
image.png
在使用时,我们只需要将其以插件的形式引入:

  1. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  2. module.exports = {
  3. plugins: [
  4. new BundleAnalyzerPlugin()
  5. ]
  6. }

(5)拆分资源

这点仍然围绕 DllPlugin 展开,可参考上文。

(6)删除冗余代码Tree-Shaking

一个比较典型的应用,就是 Tree-Shaking。
从 webpack2 开始,webpack 原生支持了 ES6 的模块系统,并基于此推出了 Tree-Shaking。
基于 import/export 语法,Tree-Shaking 可以在编译的过程中获悉哪些模块并没有真正被使用,这些没用的代码,在最后打包的时候会被去除。
举个🌰,假设我的主干文件(入口文件)是这么写的:

  1. import { page1, page2 } from './pages'
  2. // show是事先定义好的函数,大家理解它的功能是展示页面即可
  3. show(page1)

pages 文件里,我虽然导出了两个页面:

  1. export const page1 = xxx
  2. export const page2 = xxx

但因为 page2 事实上并没有被用到(这个没有被用到的情况在静态分析的过程中是可以被感知出来的),所以打包的结果里会把这部分直接删掉,这就是 Tree-Shaking 帮我们做的事情。

  1. export const page2 = xxx;

相信大家不难看出,Tree-Shaking 的针对性很强,它更适合用来处理模块级别的冗余代码。至于粒度更细的冗余代码的去除,往往会被整合进 JS 或 CSS 的压缩或分离过程中。

这里我们以当下接受度较高的 UglifyJsPlugin 为例,看一下如何在压缩过程中对碎片化的冗余代码(如 console 语句、注释等)进行自动化删除:

  1. const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
  2. module.exports = {
  3. plugins: [
  4. new UglifyJsPlugin({
  5. // 允许并发
  6. parallel: true,
  7. // 开启缓存
  8. cache: true,
  9. compress: {
  10. // 删除所有的console语句
  11. drop_console: true,
  12. // 把使用多次的静态值自动定义为变量
  13. reduce_vars: true,
  14. },
  15. output: {
  16. // 不保留注释
  17. comment: false,
  18. // 使输出的代码尽可能紧凑
  19. beautify: false
  20. }
  21. })
  22. ]
  23. }

有心的同学会注意到,这段手动引入 UglifyJsPlugin 的代码其实是 webpack3 的用法,webpack4 现在已经默认使用 uglifyjs-webpack-plugin 对代码做压缩了——在 webpack4 中,我们是通过配置 optimization.minimize 与 optimization.minimizer 来自定义压缩相关的操作的。
这里也引出了我们学习性能优化的一个核心的理念——用什么工具,怎么用,并不是我们这本小册的重点,因为所有的工具都存在用法迭代的问题。但现在大家知道了在打包的过程中做一些如上文所述的“手脚”可以实现打包结果的最优化,那下次大家再去执行打包操作,会不会对这个操作更加留心,从而自己去寻找彼时操作的具体实现方案呢?我最希望大家掌握的技能就是,先在脑海中留下“这个xx操作是对的,是有用的”,在日后的实践中,可以基于这个认知去寻找把正确的操作落地的具体方案。

(7)按需加载

大家想象这样一个场景。我现在用 React 构建一个单页应用,用 React-Router 来控制路由,十个路由对应了十个页面,这十个页面都不简单。如果我把这整个项目打一个包,用户打开我的网站时,会发生什么?有很大机率会卡死,对不对?更好的做法肯定是先给用户展示主页,其它页面等请求到了再加载。当然这个情况也比较极端,但却能很好地引出按需加载的思想:

  • 一次不加载完所有的文件内容,只加载此刻需要用到的那部分(会提前做拆分)
  • 当需要更多内容时,再对用到的内容进行即时加载

好,既然说到这十个 Router 了,我们就拿其中一个开刀,假设我这个 Router 对应的组件叫做 BugComponent,来看看我们如何利用 webpack 做到该组件的按需加载。

当我们不需要按需加载的时候,我们的代码是这样的:

  1. import BugComponent from '../pages/BugComponent'
  2. ...
  3. <Route path="/bug" component={BugComponent}>

为了开启按需加载,我们要稍作改动。
首先 webpack 的配置文件 要走起来:

  1. output: {
  2. path: path.join(__dirname, '/../dist'),
  3. filename: 'app.js',
  4. publicPath: defaultSettings.publicPath,
  5. // 指定 chunkFilename
  6. chunkFilename: '[name].[chunkhash:5].chunk.js',
  7. },

路由处的代码也要做一下配合:

  1. const getComponent => (location, cb) {
  2. require.ensure([], (require) => {
  3. cb(null, require('../pages/BugComponent').default)
  4. }, 'bug')
  5. },
  6. ...
  7. <Route path="/bug" getComponent={getComponent}>

对,核心就是这个方法:

  1. require.ensure(dependencies, callback, chunkName)

这是一个异步的方法,webpack 在打包时,BugComponent 会被单独打成一个文件,只有在我们跳转 bug 这个路由的时候,这个异步方法的回调才会生效,才会真正地去获取 BugComponent 的内容。这就是按需加载。

按需加载的粒度,还可以继续细化,细化到更小的组件、细化到某个功能点,都是 ok 的。
等等,这和说好的不一样啊?不是说 Code-Splitting 才是 React-Router 的按需加载实践吗?
没错,在 React-Router4 中,我们确实是用 Code-Splitting 替换掉了楼上这个操作。而且如果有使用过 React-Router4 实现过路由级别的按需加载的同学,可能会对 React-Router4 里用到的一个叫“Bundle-Loader”的东西印象深刻。我想很多同学读到按需加载这里,心里的预期或许都是时下大热的 Code-Splitting,而非我呈现出来的这段看似“陈旧”的代码。
但是,如果大家稍微留个心眼,去看一下 Bundle Loader 并不长的源代码的话,你会发现它竟然还是使用 require.ensure 来实现的——这也是我要把 require.ensure 单独拎出来的重要原因。所谓按需加载,根本上就是在正确的时机去触发相应的回调。理解了这个 require.ensure 的玩法,大家甚至可以结合业务自己去修改一个按需加载模块来用。
这也应了我之前跟大家强调那段话,工具永远在迭代,唯有掌握核心思想,才可以真正做到举一反三——唯“心”不破!

(二)彩蛋:Gzip 压缩原理

前面说了不少 webpack 的故事,目的还是帮大家更好地实现压缩和合并。说到压缩,可不只是构建工具的专利。我们日常开发中,其实还有一个便宜又好用的压缩操作:开启 Gzip。
具体的做法非常简单,只需要你在你的 request headers 中加上这么一句:

  1. accept-encoding:gzip

相信很多同学对 Gzip 也是了解到这里。之所以为大家开这个彩蛋性的小节,绝不是出于炫技要来给大家展示一下 Gzip 的压缩算法,而是想和大家聊一个和我们前端关系更密切的话题:HTTP 压缩。

HTTP 压缩是一种内置到网页服务器和网页客户端中以改进传输速度和带宽利用率的方式。在使用 HTTP 压缩的情况下,HTTP 数据在从服务器发送前就已压缩:兼容的浏览器将在下载所需的格式前宣告支持何种方法给服务器;不支持压缩方法的浏览器将下载未经压缩的数据。最常见的压缩方案包括 Gzip 和 Deflate。

以上是摘自百科的解释,事实上,大家可以这么理解:
HTTP 压缩就是以缩小体积为目的,对 HTTP 内容进行重新编码的过程
Gzip 的内核就是 Deflate,目前我们压缩文件用得最多的就是 Gzip。可以说,Gzip 就是 HTTP 压缩的经典例题。

(1)该不该用 Gzip

如果你的项目不是极端迷你的超小型文件,我都建议你试试 Gzip。
有的同学或许存在这样的疑问:压缩 Gzip,服务端要花时间;解压 Gzip,浏览器要花时间。中间节省出来的传输时间,真的那么可观吗?
答案是肯定的。如果你手上的项目是 1k、2k 的小文件,那确实有点高射炮打蚊子的意思,不值当。但更多的时候,我们处理的都是具备一定规模的项目文件。实践证明,这种情况下压缩和解压带来的时间开销相对于传输过程中节省下的时间开销来说,可以说是微不足道的。

(2)Gzip 是万能的吗

首先要承认 Gzip 是高效的,压缩后通常能帮我们减少响应 70% 左右的大小。
但它并非万能。Gzip 并不保证针对每一个文件的压缩都会使其变小。
Gzip 压缩背后的原理,是在一个文本文件中找出一些重复出现的字符串、临时替换它们,从而使整个文件变小。根据这个原理,文件中代码的重复率越高,那么压缩的效率就越高,使用 Gzip 的收益也就越大。反之亦然。

(3)webpack 的 Gzip 和服务端的 Gzip

一般来说,Gzip 压缩是服务器的活儿:服务器了解到我们这边有一个 Gzip 压缩的需求,它会启动自己的 CPU 去为我们完成这个任务。而压缩文件这个过程本身是需要耗费时间的,大家可以理解为我们以服务器压缩的时间开销和 CPU 开销(以及浏览器解析压缩文件的开销)为代价,省下了一些传输过程中的时间开销。
既然存在着这样的交换,那么就要求我们学会权衡。服务器的 CPU 性能不是无限的,如果存在大量的压缩需求,服务器也扛不住的。服务器一旦因此慢下来了,用户还是要等。Webpack 中 Gzip 压缩操作的存在,事实上就是为了在构建过程中去做一部分服务器的工作,为服务器分压。
因此,这两个地方的 Gzip 压缩,谁也不能替代谁。它们必须和平共处,好好合作。作为开发者,我们也应该结合业务压力的实际强度情况,去做好这其中的权衡。

(三)小结

说了这么多,我们都在讨论文件——准确地说,是文本文件及其构建过程的优化。
但一个完整的现代前端应用,除了要包含 HTML、CSS 和 JS,往往还需要借助图片来提高用户的视觉体验。而图片优化的思路、场景与措施,又是另外一个说来话长的故事了。下面,我们就一起进入图片的小天地,一窥究竟。

3.图片优化—质量与性能

《高性能网站建设指南》的作者 Steve Souders 曾在 2013 年的一篇 博客 中提到:
但是我认为 JS 和 CSS 只是展示图片的方式。在页面加载的过程中,应当先让图片和文字先展示,而不是试图保证 JS 和 CSS 更快下载完成。

雅虎军规和 Google 官方的最佳实践也都将图片优化列为前端性能优化必不可少的环节——图片优化的优先级可见一斑


优点 场景 缺点
JPEG/JPG 适合色彩丰富的图片,banner图,有损压缩、体积小、加载快、不支持透明; 经常作为大的背景图、轮播图或 Banner 图出现 处理矢量图形和 Logo 等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显。
此外,JPEG 图像不支持透明度处理,透明图片需要召唤 PNG 来呈现,不合适图形文字,图标(纹理有锯齿),不支持透明度,

PNG

| 分为 PNG-8 与 PNG-24,无损压缩、质量高、体积大、支持透明 | 追求最佳的显示效果,不在意文件体积大小时,是推荐使用 PNG-24; PNG-8更小巧; PNG 在处理线条和颜色对比度方面的优势,我们主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等 | 体积太大 | |

SVG

| 矢量图,文本文件、体积小、不失真、兼容性好,可压缩性更强,图片可无限放大而不失真,SVG 是文本文件。较强的灵活性 | 写在 HTML 里、成为 DOM 的一部分,也可以把对图形的描述写入以 .svg 为后缀的独立文件,适合小且色彩单一的图标
| 渲染成本高, | |

Base64

| 文本文件、依赖编码、小图标解决方案,Base64 是作为雪碧图的补充而存在的,base64是字符串 | Logo

何不把大图也换成 Base64 呢?图片大小会膨胀为原文件的 4/3

提供在线编解码 Base64 编解码工具很多 |

Base64 是一种用于传输 8Bit 字节码的编码方式,通过对图片进行
Base64 编码,我们可以直接将编码结果写入 HTML 或者写入 CSS,从而减少 HTTP 请求的次数 | |

WebP

| 加快图片加载速度的图片格式,它支持有损压缩和无损压缩,质量好体积小 | 站点确实是先进行了兼容性的预判,在浏览器环境支持 WebP 的情况下,优先使用 WebP 格式,否则就把图片降级为 JPG 格式 | 兼容性, Safari 等浏览器下它无法显示,淘宝处理兼容性的方式是chrome中:.webp 前面,还跟了一个 .jpg 后缀;图片应该至少存在 jpg 和 webp 两种格式,程序会根据浏览器的型号、以及该型号是否支持 WebP 这些信息来决定当前浏览器显示的是 .webp 后缀还是 .jpg 后缀,而在safari中只是 jpg; |

雪碧图、CSS 精灵、CSS Sprites、图像精灵,说的都是这个东西——一种将小图标和背景图像合并到一张图片上,然后利用 CSS 的背景定位来显示其中的每一部分的技术

  1. PNG:适合纯色 透明 图标 支持半透明; 不合适色彩丰富的图片,因为无损存储会导致存储体积大
  2. GIF:适合动画,可以动的图标;不支持半透明,不适合存储彩色图片
  3. WebP:适合半透明,可保证图片质量和较小的体积

4.缓存

网络获取内容既速度缓慢又开销巨大还具有不确定性。较大的响应需要在客户端与服务器之间进行多次往返通信,这会延迟浏览器获得和处理内容的时间,还会增加访问者的流量费用。因此,缓存并重复利用之前获取的资源的能力成为性能优化的一个关键方面。

浏览器缓存机制有四个方面:

  1. Memory Cache()
  2. Service Worker Cache
  3. HTTP Cache( Cache-Control、expires 等字段控制的缓存,强缓存协商缓存)
  4. Push Cache

HTTP Cache

它又分为强缓存协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存

1 强缓存

(1)Expires 和 Cache-Control
强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。 状态码为 200
expires : 时间戳
cache-control: max-age=31536000
Cache-Control 的 max-age 配置项相对于 expires 的优先级更高。当 Cache-Control 与 expires 同时出现时,我们以 Cache-Control 为准。

(2)s-maxage 和 max-age
s-maxage 优先级高于 max-age,两者同时出现时,优先考虑 s-maxage。如果 s-maxage 未过期,则向代理服务器请求其缓存内容。
在依赖各种代理的大型架构中,我们不得不考虑代理服务器的缓存问题。s-maxage 就是用于表示 cache 服务器上(比如 cache CDN)的缓存的有效时间的,并只对 public 缓存有效。
s-maxage仅在代理服务器中生效,客户端中我们只考虑max-age

(3) public和private
为资源设置了 public,那么它既可以被浏览器(用户端)缓存,也可以被代理服务器缓存;
设置了 private,则该资源只能被浏览器缓存(用户端),默认值

(4)no-cache和no-store
设置了 no-cache 后,不强缓存,会走协商缓存
no-store ,不使用任何缓存策略

2 协商缓存

协商缓存:资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304

(1)Last-Modified
Last-Modified 是一个时间戳
每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值,服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致;如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;没变化会 304 响应,Response Headers 不会再添加 Last-Modified 字段。

两个缺点:

  1. 编辑了文件,但文件的内容没有改变,但它仍然通过最后编辑时间进行判断。会被当做新资源
  2. 修改文件的速度过快时(比如花了 100ms 完成了改动),由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,它感知不到这个改动

(2)etag
Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,反之亦然。因此 Etag 能够精准地感知文件的变化。
缺点:Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端,Etag 并不能替代 Last-Modified,它只能作为 Last-Modified 的补充和强化存在。

Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。
image.png
资源内容不可复用时,直接为 Cache-Control 设置 no-store,拒绝一切形式的缓存;
否则考虑是否每次都需要向服务器进行缓存有效确认,如果需要,那么设 Cache-Control 的值为 no-cache;
否则考虑该资源是否可以被代理服务器缓存,根据其结果决定是设置为 private 还是 public;
然后考虑该资源的过期时间,设置对应的 max-age 和 s-maxage 值;
最后,配置协商缓存需要用到的 Etag、Last-Modified 等参数。

MemoryCache

存在内存中的缓存,浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。

资源存不存内存,浏览器秉承的是“节约原则”。我们发现,Base64 格式的图片,几乎永远可以被塞进 memory cache,这可以视作浏览器为节省渲染开销的“自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的几率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们往往被直接甩进磁盘。

Service Worker Cache

Service Worker 是一种独立于主线程之外的 Javascript 线程,可以帮我们实现离线缓存、消息推送和网络代理等功能
通过实战的方式,一起见识一下 Service Worker 如何为我们实现离线缓存:

  1. window.navigator.serviceWorker.register('/test.js').then(
  2. function () {
  3. console.log('注册成功')
  4. }).catch(err => {
  5. console.error("注册失败")
  6. })

假设我们需要缓存的文件分别是 test.html,test.css 和 test.js:

  1. // Service Worker会监听 install事件,我们在其对应的回调里可以实现初始化的逻辑
  2. self.addEventListener('install', event => {
  3. event.waitUntil(
  4. // 考虑到缓存也需要更新,open内传入的参数为缓存的版本号
  5. caches.open('test-v1').then(cache => {
  6. return cache.addAll([
  7. // 此处传入指定的需缓存的文件名
  8. '/test.html',
  9. '/test.css',
  10. '/test.js'
  11. ])
  12. })
  13. )
  14. })
  15. // Service Worker会监听所有的网络请求,网络请求的产生触发的是fetch事件,我们可以在其对应的监听函数中实现对请求的拦截,进而判断是否有对应到该请求的缓存,实现从Service Worker中取到缓存的目的
  16. self.addEventListener('fetch', event => {
  17. event.respondWith(
  18. // 尝试匹配该请求对应的缓存值
  19. caches.match(event.request).then(res => {
  20. // 如果匹配到了,调用Server Worker缓存
  21. if (res) {
  22. return res;
  23. }
  24. // 如果没匹配到,向服务端发起这个资源请求
  25. return fetch(event.request).then(response => {
  26. if (!response || response.status !== 200) {
  27. return response;
  28. }
  29. // 请求成功的话,将请求缓存起来。
  30. caches.open('test-v1').then(function(cache) {
  31. cache.put(event.request, response);
  32. });
  33. return response.clone();
  34. });
  35. })
  36. );
  37. });

Server Worker 对协议是有要求的,必须以 https 协议为前提

Push Cache

Push Cache 是指 HTTP2 在 server push 阶段存在的缓存
浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache

5.本地存储— Cookie 到 Web Storage、IndexedDB

Local Storage、Session Storage 和 Cookie 都遵循同源策略

Cookie

Cookie 的本职工作并非本地存储,而是“维持状态”。因为HTTP 无状态
附着在 HTTP 请求上,在浏览器和服务器之间“飞来飞去”。它可以携带用户信息,当服务器检查 Cookie 的时候,便可以获取到客户端的状态。
缺点:
1 当 Cookie 超过 4KB 时,它将面临被裁切的命运,
2 过量的 Cookie 会带来巨大的性能浪费,同一个域名下的所有请求,都会携带 Cookie
让“专业的人做专业的事情”,Web Storage 出现了

Web Storage

|

Local Storage

|

Session Storage

| | —- | —- | | 持久化的本地存储,永远不会过期的,手动删除; | 临时性的本地存储,会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。 | | | 即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便无法共享 | | 存储一些内容稳定的资源。比如图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串,有的网站还会用它存储一些不经常更新的 CSS、JS 等静态资源。 | 存储生命周期和它同步的会话级别的信息,比如微博的 Session Storage 就主要是存储你本次会话的浏览足迹, |

共同点:
5-10M ,仅位于浏览器端
保存的数据内容和 Cookie 一样,是文本内容,以键值对的形式存在
setItem,getItem,removeItem,clear

IndexedDB

IndexedDB 是一个运行在浏览器上的非关系型数据库,理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250M)。它不仅可以存储字符串,还可以存储二进制数据。
使用:
1 打开/创建一个 IndexedDB 数据库

  1. // 后面的回调中,我们可以通过event.target.result拿到数据库实例
  2. let db
  3. // 参数1位数据库名,参数2为版本号
  4. const request = window.indexedDB.open("xiaoceDB", 1)
  5. // 使用IndexedDB失败时的监听函数
  6. request.onerror = function(event) {
  7. console.log('无法使用IndexedDB')
  8. }
  9. // 成功
  10. request.onsuccess = function(event){
  11. // 此处就可以获取到db实例
  12. db = event.target.result
  13. console.log("你打开了IndexedDB")
  14. }

2 创建一个 object store(数据库中的“表”)

  1. // onupgradeneeded事件会在初始化数据库/版本发生更新时被调用,我们在它的监听函数中创建object store
  2. request.onupgradeneeded = function(event){
  3. let objectStore
  4. // 如果同名表未被创建过,则新建test表
  5. if (!db.objectStoreNames.contains('test')) {
  6. objectStore = db.createObjectStore('test', { keyPath: 'id' })
  7. }
  8. }

3 构建一个事务来执行一些数据库操作,像增加或提取数据等

  1. // 创建事务,指定表格名称和读写权限
  2. const transaction = db.transaction(["test"],"readwrite")
  3. // 拿到Object Store对象
  4. const objectStore = transaction.objectStore("test")
  5. // 向表格写入数据
  6. objectStore.add({id: 1, name: 'xiuyan'})

4 通过监听正确类型的事件以等待操作完成。

  1. // 操作成功时的监听函数
  2. transaction.oncomplete = function(event) {
  3. console.log("操作成功")
  4. }
  5. // 操作失败时的监听函数
  6. transaction.onerror = function(event) {
  7. console.log("这里有一个Error")
  8. }

可以创建多个数据库,一个数据库中创建多张表,一张表中存储多条数据——这足以 hold 住复杂的结构性数据, LocalStorage 无法解决的程度,我们毫无疑问可以请出 IndexedDB 来帮忙。

6.CDN 的缓存与回源机制解析

CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响。

CDN 的核心点有两个,一个是缓存,一个是回源。“缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程

CDN 往往被用来存放静态资源生成动态页面或返回非纯静态页面,

“静态资源”,就是像 JS、CSS、图片等不需要业务服务器进行计算即得的资源
“动态资源”,顾名思义是需要后端实时动态生成的资源,较为常见的就是 JSP、ASP 或者依赖服务端渲染得到的 HTML 页面。

“非纯静态资源”是指需要服务器在页面之外作额外计算的 HTML 页面。具体来说,当我打开某一网站之前,该网站需要通过权限认证等一系列手段确认我的身份、进而决定是否要把 HTML 页面呈现给我。

静态资源本身具有访问频率高、承接流量大的特点,因此静态资源加载速度始终是前端性能的一个非常关键的指标。CDN 是静态资源提速的重要手段
image.png

CDN 优化细节

涉及到 CDN 服务器本身的性能优化、CDN 节点的地址选取等
Cookie 是紧跟域名的。同一个域名下的所有请求,都会携带 Cookie
静态资源往往并不需要 Cookie 携带什么认证信息。把静态资源和主页面置于不同的域名下,完美地避免了不必要的 Cookie 的出现

看起来是一个不起眼的小细节,但带来的效用却是惊人的。以电商网站静态资源的流量之庞大,如果没把这个多余的 Cookie 拿下来,不仅用户体验会大打折扣,每年因性能浪费带来的经济开销也将是一个非常恐怖的数字。

7.服务端渲染SSR

“是什么”(服务端渲染的运行机制)、“为什么”(服务端渲染解决了什么性能问题 )、“怎么做”(服务端渲染的应用实例与使用场景)这三个点,对服务端渲染(SSR)进行探索

是什么?

客户端渲染模式下,服务端会把渲染需要的静态文件发送给客户端,客户端加载过来之后,自己在浏览器里跑一遍 JS,根据 JS 的运行结果,生成相应的 DOM。页面上呈现的内容,在 html 源文件里里找不到——这正是它的特点。

服务端渲染,用户第一次请求页面时,由服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端,页面上呈现的内容,我们在 html 源文件里也能找到

SPA模式下的SSR的核心是同构:路由同构,预取同构,渲染同构。

像动态页面缺点其实就是你首屏还是需要去做一次异步请求拉一下数据再去渲染就会产生一个页面抖动

ssr处理其实就是在我们建立完TCP连接发起http请求之后网关层面做一些转发的时候我们可以在这时候由服务器去拉取数据,返回已经有数据的html页面。由于服务器之间的请求更加稳定高效所以效果其实是很好的,但是有个问题就是服务器去拉数据的话还是有可能失败的超时的,所以前端还是需要做一个补偿,例如onload后再去异步请求数据。还有就是这个服务端的拉取数据的方式利用redis缓存的话平均耗时要比http请求快10倍左右

用框架比如nuxt做的ssr 似乎接口如果超时的话 会一直白屏 不会走到onload吧:那就要靠服务端做额外逻辑兼容了

解决了什么问题?

很多网站是出于效益的考虑才启用服务端渲染,性能倒是在其次。

效益:搜索引擎只会查找现成的内容,不会帮你跑 JS 代码。搜索引擎搜不出来,用户找不到我们,谁还会用我的网站呢?为了把“现成的内容”拿给搜索引擎看,有时候网站不得不启用服务端渲染。

性能:服务端渲染解决了一个非常关键的性能问题——首屏加载速度过慢
客户端渲染:除了加载 HTML,还要等渲染所需的这部分 JS 加载完,之后还得把这部分 JS 在浏览器上再跑一遍,这个过程结束之前用户一直在等
服务端渲染模式下,服务器给到客户端的已经是一个直接可以拿来呈现给用户的网页,中间环节早在服务端就帮我们做掉了。

实现方法

react实现

  1. import React from 'react'
  2. const VDom = () => {
  3. return <div>我是一个被渲染为真实DOM的虚拟DOM</div>
  4. }
  5. export default VDom
  1. import express from 'express'
  2. import React from 'react'
  3. import { renderToString } from 'react-dom/server'
  4. import VDom from './VDom'
  5. // 创建一个express应用; 使用 Express 搭建后端服务
  6. const app = express()
  7. // renderToString 是把虚拟DOM转化为真实DOM的关键方法
  8. const RDom = renderToString(<VDom />)
  9. // 编写HTML模板,插入转化后的真实DOM内容
  10. const Page = `
  11. <html>
  12. <head>
  13. <title>test</title>
  14. </head>
  15. <body>
  16. <span>服务端渲染出了真实DOM: </span>
  17. ${RDom}
  18. </body>
  19. </html>
  20. `
  21. // 配置HTML内容对应的路由
  22. app.get('/index', function(req, res) {
  23. res.send(Page)
  24. })
  25. // 配置端口号
  26. const server = app.listen(8000)

image.png

基于 Vue SSR 实现
vue中服务器端渲染只支持 beforeCreate 和 created 两个钩子

  1. const Vue = require('vue')
  2. // 创建一个express应用,使用 Express 搭建后端服务
  3. const server = require('express')()
  4. // 提取出renderer实例
  5. const renderer = require('vue-server-renderer').createRenderer()
  6. server.get('*', (req, res) => {
  7. // 编写Vue实例(虚拟DOM节点)
  8. const app = new Vue({
  9. data: {
  10. url: req.url
  11. },
  12. // 编写模板HTML的内容
  13. template: `<div>访问的 URL 是: {{ url }}</div>`
  14. })
  15. // renderToString 是把Vue实例转化为真实DOM的关键方法
  16. renderer.renderToString(app, (err, html) => {
  17. if (err) {
  18. res.status(500).end('Internal Server Error')
  19. return
  20. }
  21. // 把渲染出来的真实DOM字符串插入HTML模板中
  22. res.end(`
  23. <!DOCTYPE html>
  24. <html lang="en">
  25. <head><title>Hello</title></head>
  26. <body>${html}</body>
  27. </html>
  28. `)
  29. })
  30. })
  31. server.listen(8080)

以上两个例子展示了基本的服务端渲染实现流程

一是这个 renderToString() 方法(转换成真实dom的关键方法);
二是把转化结果“塞”进模板里的这一步。
这两个操作是服务端渲染的灵魂操作。在虚拟 DOM“横行”的当下,服务端渲染不再是早年 JSP 里简单粗暴的字符串拼接过程,它还要求这一端要具备将虚拟 DOM 转化为真实 DOM 的能力。与其说是“把 JS 在服务器上先跑一遍”,不如说是“把 Vue、React 等框架代码先在 Node 上跑一遍”。

使用场景

服务端渲染本质上是本该浏览器做的事情,分担给服务器去做。这样当资源抵达浏览器时,它呈现的速度就快了。

用户拥有的浏览器总量多到数不清,那么一个公司的服务器又有多少台呢?这么多台浏览器的渲染压力集中起来,分散给相比之下数量并不多的服务器,服务器肯定是承受不住的。

修言老师建议大家先忘记服务端渲染这个事情——服务器稀少而宝贵,但首屏渲染体验和 SEO 的优化方案却很多——我们最好先把能用的低成本“大招”都用完。除非网页对性能要求太高了,以至于所有的招式都用完了,性能表现还是不尽人意,这时候我们就可以考虑向老板多申请几台服务器,把服务端渲染搞起来了~

8.浏览器背后的运行机制—浏览器的工作原理

浏览器内核

浏览器的“心”,说的就是浏览器的内核。浏览器内核决定了浏览器解释网页语法的方式。
image.png

其中GUI渲染线程又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件。
image.png

每一帧

渲染过程:html,css,js资源 —-》浏览器内核—-》图像

浏览器内核和每一帧做的事 结合理解
内核指代的是JS引擎(js线程)和 渲染引擎(渲染进程),由于JS引擎越来越独立,所以现在的内核大部分是指代渲染引擎,渲染进程包含http请求线程,事件触发线程,定时器触发线程,GUI线程,还有与gui线程互斥的js线程(被独立出去的JS引擎)。
gui渲染线程主要利用各种解释器是收集html,css分别解析成DOM树和CSSOM树,根据计算出来的渲染树每一“帧”去实现布局并绘制渲染。每一帧里做的事情包括处理输入事件,执行事件回调,开始帧,执行requestAnimationFrame,页面布局,样式计算,绘制渲染,如果还有剩下的时间就执行requestIdleCallback
image.png
需要注意:HTML 解释器CSS 解释器图层布局计算模块视图绘制模块JavaScript 线程这几大模块:

  • HTML 解释器:将 HTML 文档经过词法分析输出 DOM 树。
  • CSS 解释器:解析 CSS 文档, 生成样式规则。
  • 图层布局计算模块:布局计算每个对象的精确位置和大小。
  • 视图绘制模块:进行具体节点的图像绘制,将像素渲染到屏幕上。
  • JavaScript 引擎:编译执行 Javascript 代码。

页面的首次渲染

image.png

  • 解析 HTML:发出了页面渲染所需的各种外部资源请求—dom树。
  • 计算样式:识别并加载所有的 CSS 样式信息(cssom树)与 DOM 树合并,生成页面 render 树( 这样的伪元素会在这个环节被构建到树中)
  • 计算图层布局:所有元素的相对位置信息,大小等信息均在这一步得到计算。
  • 绘制图层:根据我们的 DOM 代码结果,把每一个页面图层转换为像素,并对所有的媒体文件进行解码。
  • 整合图层,得到页面:合并各个图层,将数据由 CPU 输出给 GPU 最终绘制在屏幕上。(复杂的视图层会给这个阶段的 GPU 计算带来一些压力,在实际应用中为了优化动画性能,我们有时会手动区分不同的图层,比如开启硬件加速:GPU加速transform: translateZ(0);will-change开启gpu加速,…..)。

树以及更新

image.png

  • DOM 树:渲染引擎解析 HTML 以创建的是 DOM 树
  • CSSOM 树:css解析器解析 CSS(包括外部 CSS 文件和样式元素)创建的是 CSSOM 树。CSSOM 的解析过程与 DOM 的解析过程是并行的
  • 渲染树:CSSOM 与 DOM 结合,之后我们得到的就是渲染树(Render tree )。
  • 布局渲染树:从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标,我们便得到了基于渲染树的布局渲染树(Layout of the render tree)。
  • 绘制渲染树: 遍历渲染树,每个节点将使用 UI 后端层来绘制。整个过程叫做绘制渲染树(Painting the render tree)。

之后每当一个新元素加入到这个 DOM 树当中,浏览器便会通过 CSS 引擎查遍 CSS 样式表,找到符合该元素的样式规则应用到这个元素上,然后再重新去绘制它。

基渲染流程 CSS 优化

CSS 引擎查找样式表,对每条规则都按从右到左的顺序去匹配

  1. #myList li {
  2. color:''
  3. }

CSS 选择符是从右到左进行匹配的。我们这个看似“没毛病”的选择器,实际开销相当高:浏览器必须遍历页面上每个 li 元素,并且每次都要去确认这个 li 元素的父元素 id 是不是 myList

通配符:它会匹配所有元素,所以浏览器必须去遍历每一个元素!这得计算多少次呀!

  1. * {
  2. }

好的 CSS 选择器书写习惯,可以为我们带来非常可观的性能提升
性能提升的方案:

  • 避免使用通配符,只对需要用到的元素进行选择。
  • 关注可以通过继承实现的属性,避免重复匹配重复定义。
  • 少用标签选择器。如果可以,用类选择器替代
  • 不要画蛇添足,id 和 class 选择器不应该被多余的标签选择器拖后腿
  • 减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素。

    告别阻塞:CSS 与 JS 的加载顺序优化

    HTML、CSS 和 JS,都具有阻塞渲染的特性。

    CSS 的阻塞

    DOM 和 CSSOM 合力才能构建渲染树,默认情况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容。即便 DOM 已经解析完毕了,只要 CSSOM 不 OK,那么渲染这个事情就不 OK(这主要是为了避免没有 CSS 的 HTML 页面丑陋地“裸奔”在用户眼前)

只有当我们开始解析 HTML 后、解析到 link 标签或者 style 标签时,CSS 才登场,CSSOM 的构建才开始。很多时候,DOM 不得不等待 CSSOM。

CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现静态资源加载速度的优化),“把 CSS 往前放”的动作,它是由 CSS 的特性决定的。

JS 的阻塞

在首次渲染过程中,JS 并不是一个非登场不可的角色——没有 JS,CSSOM 和 DOM 照样可以组成渲染树,页面依然会呈现——即使它死气沉沉、毫无交互。

JS 的作用在于修改,本质上都是对 DOM 和 CSSDOM 进行修改。因此 JS 的执行会阻止 CSSOM,在我们不作显式声明的情况下,它也会阻塞 DOM。JS 线程是独立于GUI渲染引擎存在,他们互斥。

遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了GUI渲染线程的控制权。

浏览器之所以让 JS 阻塞其它的活动,是因为它不知道 JS 会做什么改变,担心如果不阻止后续的操作,会造成混乱,js可以操作dom。操作了dom又要重新渲染 阻塞渲染一是为了节省性能,但是我们是写 JS 的人,我们知道 JS 会做什么改变。假如我们可以确认一个 JS 文件的执行时机并不一定非要是此时此刻,我们就可以通过对它使用 defer 和 async 来避免不必要的阻塞或者是放到底部,这里我们就引出了外部 JS 的三种加载方式。

JS的三种加载方式

  • 正常模式:JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去做其它事情。
  • async 模式:async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行,执行顺序不确定。 DOM 元素和其它脚本之间的依赖关系不强时,我们会选用 async
  • defer 模式:JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。 DOM 元素和其它脚本的执行结果时,我们会选用 defer


通过审时度势地向 script 标签添加 async/defer,我们就可以告诉浏览器在等待脚本可用期间不阻止其它的工作,这样可以显著提升性能。

9.DOM 优化—原理和基本实践

“DOM 为什么这么慢”以及“如何使 DOM 变快”?
“DOM 优化思路”、“异步更新策略”及“回流与重绘”

DOM 为什么这么慢

当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口作为“桥梁”,过“桥”要收费——这个开销本身就是不可忽略的。过“桥”的次数一多,就会产生比较明显的性能问题。因此“减少 DOM 操作”的建议,并非空穴来风。

对 DOM 的修改引发样式的更迭

当我们对 DOM 的修改会引发它外观(样式)上的改变时,就会触发回流重绘

  • 回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
  • 重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。

重绘不一定导致回流,回流一定会导致重绘。硬要比较的话,回流比重绘做的事情更多,带来的开销也更大。尽可能把回流和重绘的次数最小化。

给你的 DOM “提提速

JS 层面的事情,JS 自己去处理,处理好了,再来找 DOM 打报告。—让 JS 去给 DOM 分压—

  1. DocumentFragment不是真实 DOM 树的一部分,它的变化不会引起 DOM 树的重新渲染的操作(reflow),且不会导致性能等问题,更加结构化的方式去达成同样的目的,从而在维持性能的同时,保住我们代码的可拓展和可维护性。

  2. 直接用 innerHTML 去拼接目标内容,这样做固然有用,但却不够优雅

DOM Fragment 对象允许我们像操作真实 DOM 一样去调用各种各样的 DOM API,我们的代码质量因此得到了保证。并且它的身份也非常纯粹:当我们试图将其 append 进真实 DOM 时,它会在乖乖交出自身缓存的所有后代节点后全身而退,完美地完成一个容器的使命,而不会出现在真实的 DOM 结构中。这种结构化、干净利落的特性,使得 DOM Fragment 作为经典的性能优化手段大受欢迎,这一点在 jQuery、Vue 等优秀前端框架的源码中均有体现。

10.DOM 优化—Event Loop 与异步更新策略

异步更新策略,都达到了减少 DOM 操作、避免过度渲染的目的。

Event Loop 中的“渲染时机”

常见的 macro-task 比如: setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。
常见的 micro-task 比如: process.nextTick、Promise、MutationObserver ,async/await等。

Event Loop 过程解析

一个完整的 Event Loop 过程,可以概括为以下阶段:

  • 初始状态:调用栈空:micro (队列空),macro (队列里有且只有一个 script 脚本(整体代码))。
  • 开始执行:全局上下文(script 标签)被推入调用栈,同步代码执行。在执行的过程中,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程
  • 出队:上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的(如下图所示)。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
  • 执行渲染操作,更新界面(敲黑板划重点)。
  • 检查是否存在 Web worker 任务,有则对其进行处理 。

(上述过程循环往复,直到两个队列都清空)
image.png
每一次循环都是一个这样的过程:

image.png

渲染的时机

我们更新 DOM 的时间点,应该尽可能靠近渲染的时机。当我们需要在异步任务中实现 DOM 修改时,把它包装成 micro 任务是相对明智的选择

实践:异步更新策略

Vue 或 React:更新数据时,这个更新并不会立即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发。这就是异步更新。避免过度渲染,是我们上节提到的“让 JS 为 DOM 分压”的典范之一

Vue状态更新手法:nextTick

Vue 每次想要更新一个状态的时候,会先把它这个更新操作给包装成一个异步操作派发出去。这件事情,在源码中是由一个叫做 nextTick 的函数来完成的,Vue 的异步任务默认情况下都是用 Promise 来包装的,也就是是说它们都是 micro-task。
macroTimeFunc() 是这么实现的:

  1. // macro首选setImmediate 这个兼容性最差
  2. if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  3. macroTimerFunc = () => {
  4. setImmediate(flushCallbacks)
  5. }
  6. } else if (typeof MessageChannel !== 'undefined' && (
  7. isNative(MessageChannel) ||
  8. // PhantomJS
  9. MessageChannel.toString() === '[object MessageChannelConstructor]'
  10. )) {
  11. const channel = new MessageChannel()
  12. const port = channel.port2
  13. channel.port1.onmessage = flushCallbacks
  14. macroTimerFunc = () => {
  15. port.postMessage(1)
  16. }
  17. } else {
  18. // 兼容性最好的派发方式是setTimeout
  19. macroTimerFunc = () => {
  20. setTimeout(flushCallbacks, 0)
  21. }
  22. }

microTimeFunc() 是这么实现的:

  1. // 简单粗暴 不是ios全都给我去Promise 如果不兼容promise 那么你只能将就一下变成macro了
  2. if (typeof Promise !== 'undefined' && isNative(Promise)) {
  3. const p = Promise.resolve()
  4. microTimerFunc = () => {
  5. p.then(flushCallbacks)
  6. if (isIOS) setTimeout(noop)
  7. }
  8. } else {
  9. // 如果无法派发micro,就退而求其次派发为macro
  10. microTimerFunc = macroTimerFunc
  11. }

无论是派发 macro 任务还是派发 micro 任务,派发的任务对象都是一个叫做 flushCallbacks 的东西

  1. function flushCallbacks () {
  2. pending = false
  3. // callbacks在nextick中出现过 它是任务数组(队列)
  4. const copies = callbacks.slice(0)
  5. callbacks.length = 0
  6. // 将callbacks中的任务逐个取出执行,对当前 callbacks 数组的任务进行派发和执行
  7. for (let i = 0; i < copies.length; i++) {
  8. copies[i]()
  9. }
  10. }
  1. Vue 中每产生一个状态更新任务,它就会被塞进一个叫 callbacks 的数组(此处是任务队列的实现形式)中。
  2. 这个任务队列在被丢进 micro 或 macro 队列之前,会先去检查当前是否有异步更新任务正在执行(即检查 pending 锁)。
  3. 如果确认 pending 锁是开着的(false),就把它设置为锁上(true)
  4. 然后对当前 callbacks 数组的任务进行派发(丢进 micro 或 macro 队列)和执行。
  5. 设置 pending 锁的意义在于保证状态更新任务的有序进行,避免发生混乱。

11.DOM 优化—回流(Reflow)与重绘(Repaint)

哪些实际操作会导致回流与重绘

回流的:

  1. 改变 DOM 元素的几何属性( width、height、padding、margin、left、top、border 等);
  2. 改变 DOM 树的结构(节点的增减、移动等操作),
  3. 获取一些特定属性的值—即时准确计算(offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight ,getComputedStyle, IE 里的 currentStyle)

    如何规避回流与重绘

    (1)以 JS 变量的形式缓存起来

    1. // 获取el元素
    2. const el = document.getElementById('el')
    3. // 这里循环判定比较简单,实际中或许会拓展出比较复杂的判定需求
    4. for(let i=0;i<10;i++) {
    5. el.style.top = el.offsetTop + 10 + "px";
    6. el.style.left = el.offsetLeft + 10 + "px";
    7. }
    ```php // 缓存offsetLeft与offsetTop的值 const el = document.getElementById(‘el’) let offLeft = el.offsetLeft, offTop = el.offsetTop

// 在JS层面进行计算 for(let i=0;i<10;i++) { offLeft += 10 offTop += 10 }

// 一次性将计算结果应用到DOM上 el.style.left = offLeft + “px” el.style.top = offTop + “px”

  1. <a name="e75aX"></a>
  2. #### (2)避免逐条改变样式,使用类名去合并样式
  3. ```php
  4. const container = document.getElementById('container')
  5. container.style.width = '100px'
  6. container.style.height = '200px'
  7. container.style.border = '10px solid red'
  8. container.style.color = 'red'
  1. const container = document.getElementById('container')
  2. container.classList.add('basic_style')

将 DOM “离线”

给元素设置 display: none—-后续操作,将无法触发回流与重绘

我们把它离线了,后续不管我操作这个元素多少次,每一步的操作成本都会非常低。当我们只需要进行很少的 DOM 操作时,DOM 离线化的优越性确实不太明显。一旦操作频繁起来,这“拿掉”和“放回”的开销都将会是非常值得的。

Flush 队列:浏览器并没有那么简单

现代浏览器(chrome)是很聪明的。浏览器自己也清楚,如果每次 DOM 操作都即时地反馈一次回流或重绘,那么性能上来说是扛不住的。于是它自己缓存了一个 flush 队列,把我们触发的回流与重绘任务都塞进去,待到队列里的任务多起来、或者达到了一定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队。

大家这里尤其小心这个“不得已”的时候。前面我们在介绍回流的“导火索”的时候,提到过有一类属性很特别,它们有很强的“即时性”。当我们访问这些属性时,浏览器会为了获得此时此刻的、最准确的属性值,而提前将 flush 队列的任务出队——这就是所谓的“不得已”时刻。—-并不是所有的浏览器都是聪明的,如果不手动做优化,那么一个页面在不同的环境下就会呈现不同的性能效果,这对我们、对用户都是不利的。因此,养成良好的编码习惯、从根源上解决问题,仍然是最周全的方法。

12.优化首屏体验

Lazy-Load 初相见

Lazy-Load,翻译过来是“懒加载”。它是针对图片加载时机的优化:
用户真的需要这么多图片吗?用户点开页面的瞬间,呈现给他的只有屏幕的一部分(我们称之为首屏)。只要我们可以在页面打开的时候把首屏的图片资源加载出来,用户就会认为页面是没问题的。至于下面的图片,我们完全可以等用户下拉的瞬间再即时去请求、即时呈现给他。这样一来,性能的压力小了,用户的体验却没有变差——这个延迟加载的过程,就是 Lazy-Load。

以尽可能快的速度,疯狂向下拉动页面,明明已经拉到目标位置了,文字也呈现完毕了,图片却慢半拍才显示出来。这是因为,掘金首页也采用了懒加载策略。当我们的页面并未滚动至包含图片的 div 元素所在的位置时,它的样式是这样的:

  1. <div data-v-b2db8566=""
  2. data-v-009ea7bb=""
  3. data-v-6b46a625=""
  4. data-src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/9/27/16619f449ee24252~tplv-t2oaga2asx-image.image"
  5. class="lazy thumb thumb"
  6. style="background-image: none; background-size: cover;">
  7. </div>


注意到 style 内联样式中,背景图片设置为了 none。也就是说这个 div 是没有内容的,它只起到一个占位的作用
现在可视区域的瞬间,div 元素的内容被即时地修改掉了——它被写入了有效的图片 URL,于是图片才得以呈现。这就是懒加载的实现思路。

写一个 Lazy-Load

Lazy-Load 的思路及实现方式为大厂面试常考题,还望诸位同学引起重视
先写一个html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>Lazy-Load</title>
  8. <style>
  9. .img {
  10. width: 200px;
  11. height:200px;
  12. background-color: gray;
  13. }
  14. .pic {
  15. // 必要的img样式
  16. }
  17. </style>
  18. </head>
  19. <body>
  20. <div class="container">
  21. <div class="img">
  22. // 注意我们并没有为它引入真实的src
  23. <img class="pic" alt="加载中" data-src="./images/1.png">
  24. </div>
  25. <div class="img">
  26. <img class="pic" alt="加载中" data-src="./images/2.png">
  27. </div>
  28. <div class="img">
  29. <img class="pic" alt="加载中" data-src="./images/3.png">
  30. </div>
  31. <div class="img">
  32. <img class="pic" alt="加载中" data-src="./images/4.png">
  33. </div>
  34. <div class="img">
  35. <img class="pic" alt="加载中" data-src="./images/5.png">
  36. </div>
  37. <div class="img">
  38. <img class="pic" alt="加载中" data-src="./images/6.png">
  39. </div>
  40. <div class="img">
  41. <img class="pic" alt="加载中" data-src="./images/7.png">
  42. </div>
  43. <div class="img">
  44. <img class="pic" alt="加载中" data-src="./images/8.png">
  45. </div>
  46. <div class="img">
  47. <img class="pic" alt="加载中" data-src="./images/9.png">
  48. </div>
  49. <div class="img">
  50. <img class="pic" alt="加载中" data-src="./images/10.png">
  51. </div>
  52. </div>
  53. </body>
  54. </html>

懒加载::一个是当前可视区域的高度,另一个是元素距离可视区域顶部的高度

方法一
当前可视区域的高度:const viewHeight = window.innerHeight || document.documentElement.clientHeight (兼容低版本 IE )
元素距离可视区域顶部的高度: getBoundingClientRect() 方法(描述边框的只读属性,top 属性代表了元素距离可视区域顶部的高度)
它们对应到元素上是这样的:
image.png

  1. <script>
  2. // 获取所有的图片标签
  3. const imgs = document.getElementsByTagName('img')
  4. // 获取可视区域的高度
  5. const viewHeight = window.innerHeight || document.documentElement.clientHeight
  6. // num用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
  7. let num = 0
  8. function lazyload(){
  9. for(let i=num; i<imgs.length; i++) {
  10. // 用可视区域高度减去元素顶部距离可视区域顶部的高度
  11. let distance = viewHeight - imgs[i].getBoundingClientRect().top
  12. // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
  13. if(distance >= 0 ){
  14. // 给元素写入真实的src,展示图片
  15. imgs[i].src = imgs[i].getAttribute('data-src')
  16. // 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
  17. num = i + 1
  18. }
  19. }
  20. }
  21. // 监听Scroll事件
  22. window.addEventListener('scroll', lazyload, false);
  23. </script>

方法二
image.png
内容达到显示区域的:img.offsetTop < window.innerHeight + document.body.scrollTop

  1. <script>
  2. // 获取所有的图片标签
  3. const imgs = document.getElementsByTagName('img')
  4. // 获取可视区域的高度
  5. const viewHeight = window.innerHeight || document.documentElement.clientHeight
  6. let num = 0// num用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
  7. function lazyload(){
  8. for(let i=num; i<imgs.length; i++) {
  9. // 如果加载在图片距离顶部 小于 可视高度与滚动高度之和
  10. if(imgs[i].offsetTop < viewHeight + document.body.scrollTop){
  11. // 给元素写入真实的src,展示图片
  12. imgs[i].src = imgs[i].getAttribute('data-src')
  13. // 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
  14. num = i + 1
  15. }
  16. }
  17. }
  18. // 监听Scroll事件
  19. window.addEventListener('scroll', lazyload, false);
  20. </script>

scroll 事件,是一个危险的事件——它太容易被触发了。用户的每一次滚动都将触发我们的监听函数。函数执行是吃性能的,频繁地响应某个事件将造成大量不必要的页面计算。因此,我们需要针对那些有可能被频繁触发的事件作进一步地优化。这里就引出了我们下一节的两位主角——throttle 与 debounce。滚动快白屏的情况可以事先画好一些div占位

13.事件的节流与防抖

不止 scroll 事件,resize 事件、鼠标事件(比如 mousemove、mouseover 等)、键盘事件(keyup、keydown 等)都存在被频繁触发的风险。
频繁触发回调导致的大量计算会引发页面的抖动甚至卡顿。

这两个东西都以闭包的形式存在。
throttle 的中心思想在于:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。

  1. function throttle(fn, delay) {
  2. let flag = true;
  3. return () => {
  4. if (!flag) return;
  5. flag = false;
  6. timer = setTimeout(() => {
  7. fn();
  8. flag = true;
  9. }, delay);
  10. };
  11. }
  1. // fn是我们需要包装的事件回调, interval是时间间隔的阈值
  2. function throttle(fn, interval) {
  3. // last为上一次触发回调的时间
  4. let last = 0
  5. // 将throttle处理结果当作函数返回
  6. return function () {
  7. // 保留调用时的this上下文
  8. let context = this
  9. // 保留调用时传入的参数
  10. let args = arguments
  11. // 记录本次触发回调的时间
  12. let now = +new Date()
  13. // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
  14. if (now - last >= interval) {
  15. // 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
  16. last = now;
  17. fn.apply(context, args);
  18. }
  19. }
  20. }
  21. // 用throttle来包装scroll的回调
  22. const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
  23. document.addEventListener('scroll', better_scroll)

防抖的中心思想在于:我会等你到底。在某段时间内,每触发一次就把上一次的计时器清零,不管你触发了多少次回调,我都只认最后一次。

  1. function debounce(fn, delay = 300) {
  2. //默认300毫秒
  3. let timer;
  4. return function () {
  5. const args = arguments;
  6. if (timer) {
  7. clearTimeout(timer);
  8. }
  9. timer = setTimeout(() => {
  10. fn.apply(this, args); // 改变this指向为调用debounce所指的对象
  11. }, delay);
  12. };
  13. }

用 Throttle 来优化 Debounce

debounce缺陷:频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。
需要借力 throttle 的思想,打造一个“有底线”的 debounce

  1. // fn是我们需要包装的事件回调, delay是时间间隔的阈值
  2. function throttle(fn, delay) {
  3. // last为上一次触发回调的时间, timer是定时器
  4. let last = 0, timer = null
  5. // 将throttle处理结果当作函数返回
  6. return function () {
  7. // 保留调用时的this上下文
  8. let context = this
  9. // 保留调用时传入的参数
  10. let args = arguments
  11. // 记录本次触发回调的时间
  12. let now = +new Date()
  13. // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
  14. if (now - last < delay) {
  15. // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
  16. clearTimeout(timer)
  17. timer = setTimeout(function () {
  18. last = now
  19. fn.apply(context, args)
  20. }, delay)
  21. } else {
  22. // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
  23. last = now
  24. fn.apply(context, args)
  25. }
  26. }
  27. }
  28. // 用新的throttle包装scroll的回调
  29. const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
  30. document.addEventListener('scroll', better_scroll)

14.Performance、LightHouse 与性能 API

性能监测方案主要有两种:可视化方案、可编程方案

可视化监测:从 Performance 面板说起

使用 Performance 工具时,为了规避其它 Chrome 插件对页面的性能影响,我们最好在无痕模式下打开页面:
image.png
我们看右上角的三个栏目:FPS、CPU 和 NET。
FPS:这是一个和动画性能密切相关的指标,它表示每一秒的帧数。图中绿色柱状越高表示帧率越高,体验就越流畅。若出现红色块,则代表长时间帧,很可能会出现卡顿。图中以绿色为主,偶尔出现红块,说明网页性能并不糟糕,但仍有可优化的空间。
CPU:表示CPU的使用情况,不同的颜色片段代表着消耗CPU资源的不同事件类型。这部分的图像和下文详情面板中的Summary内容有对应关系,我们可以结合这两者挖掘性能瓶颈。
NET:粗略的展示了各请求的耗时与前后顺序。这个指标一般来说帮助不大。
image.png

挖掘性能瓶颈

主要去看 Main 栏目下的火焰图和 Summary 提供给我们的饼图——这两者和概述面板中的 CPU 一栏结合,可以帮我们迅速定位性能瓶颈
image.png
CPU 图表中,我们可以根据颜色填充的饱满程度,确定 CPU 的忙闲,进而了解该页面的总的任务量。
而 Summary 饼图则以一种直观的方式告诉了我们,哪个类型的任务最耗时(从本例来看是脚本执行过程)。
火焰图main非常关键,它展示了整个运行时主进程所做的每一件事情(包括加载、脚本运行、渲染、布局、绘制等)。x 轴表示随时间的记录。每个长条就代表一个活动。更宽的条形意味着事件需要更长时间。y 轴表示调用堆栈,我们可以看到事件是相互堆叠的,上层的事件触发了下层的事件。

CPU 图标和 Summary 图都是按照“类型”给我们提供性能信息,而 Main 火焰图则将粒度细化到了每一个函数的调用。
到底是从哪个过程开始出问题、是哪个函数拖了后腿、又是哪个事件触发了这个函数,这些具体的、细致的问题都将在 Main 火焰图中得到解答。

可视化监测: 更加聪明的 LightHouse

能够将晦涩的数据“翻译”成具体的性能问题。
Lighthouse 是一个开源的自动化工具,用于改进网络应用的质量。 你可以将其作为一个 Chrome 扩展程序运行,或从命令行运行。 为Lighthouse 提供一个需要审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。

它生成的是一个报告!Report!不是干巴巴地数据,而是一个通过测试与分析呈现出来的结果(它甚至会给你的页面跑一个分数出来)。这个东西看起来也真是太赞了,我们这就来体验一下!
image.png
页面性能、PWA(渐进式 Web 应用)、可访问性(无障碍)、最佳实践、SEO 五项指标的跑分

向下拉动 Report 页,我们还可以看到每一个指标的细化评估
image.png

可编程的性能上报方案: W3C 性能 API

为 Performance 面板的进一步细化与可编程化。
控制台里输入 window.performance
image.png
image.png

  1. const timing = window.performance.timing
  2. // DNS查询耗时
  3. timing.domainLookupEnd - timing.domainLookupStart
  4. // TCP连接耗时
  5. timing.connectEnd - timing.connectStart
  6. // 内容加载耗时
  7. timing.responseEnd - timing.requestStart

image.png

更应该去关注一些关键性能指标:firstbyte、fpt、tti、ready 和 load 时间

  1. // firstbyte:首包时间
  2. timing.responseStart timing.domainLookupStart
  3. // fpt:First Paint Time, 首次渲染时间 / 白屏时间
  4. timing.responseEnd timing.fetchStart
  5. // tti:Time to Interact,首次可交互时间
  6. timing.domInteractive timing.fetchStart
  7. // ready:HTML 加载完成时间,即 DOM 就位的时间
  8. timing.domContentLoaded timing.fetchStart
  9. // load:页面完全加载时间
  10. timing.loadEventStart timing.fetchStart

访问 performance 的 memory 属性,我们还可以获取到内存占用相关的数据;通过对 performance 的其它属性方法的灵活运用,我们还可以把它耦合进业务里,实现更加多样化的性能监测需求——灵活,是可编程化方案最大的优点。

十五.项目里的优化

我根据lighthouse建议去进行了一些优化:(前端渲染的性能影响主要是因为浏览器渲染)
其中,说包体积太大,我就

  1. 压缩文件、使用 Tree-shaking 删除无用代码
  2. optimization.minimize压缩js, terser-webpack-plugin去除死代码,
  3. optimization.splitChunks抽离公共模块,生成公共 chunk,避免重复引用,减少体积

图片处理:

  1. image-webpack-loader优化压缩图片,图片用tinypng网站去压缩,icon使用雪碧图
  2. 图片格式如果支持WebP 就尽量使用 WebP
  3. 资源按需加载—图片懒加载,路由懒加载(异步方式分模块加载文件)
  4. 把业务逻辑甚至样式都一样的模块抽离出来封装成组件,减少冗余代码
  5. 服务端配置 Gzip 进一步再压缩文件体积(gizp压缩是一种http请求优化方式,通过减少文件体积来提高加载速度。html、js、css文件甚至json数据都可以用它压缩,可以减小60%以上的体积。前端配置gzip压缩,并且服务端使用nginx开启gzip,用来减小网络传输的流量大小。在webpack中加new CompressionWebpackPlugin(),启用gzip压缩打包之后,自动生成gz包)
  6. 用 CDN 加载资源
  7. dns-prefetch 预解析 DNS 的 IP 地址
  8. 对首屏数据做离线缓存
  9. 用webpack-bundle-analyzer做分析发现vue全家桶相关依赖占用了很大的空间,在webpack的dev开发配置文件中, 使用externals,可以分离打包第三方资源包,key为依赖包名称,value是源码抛出来的全局变量,对于一些其他的工具库,尽量采用按需引入的方式。gzipped size值减少了50kb,会发现分析后的图里没有Vue 全家桶了
  10. 生产环境关闭源码映射,一方面能减少代码包的大小,另一方面也有利于系统代码安全
  11. 对于大数据渲染 本来是全量拉取数据全量展示的,后来使用了 虚拟列表
  12. 通过性能分析/merry内存图 发现这个项目存在内存泄漏的问题,检查了之后发现一些js的箭头函数并没有得到及时释放,类似于echarts,监听交互,定时器这些没有释放,所以我在destroied中对他们清除释放,这样就减少了内存泄漏的问题

这些点主要优化了 FP、FCP、LCP 指标,

在网络请求上,

  1. 公司用的http1.1,可以让运营同学帮我去配置使用http2让网站加载的更快,但是他们拒绝了,他们担心存在一些问题,不过我知道http2中有个多路复用,一个域名下可以建立多个TCP连接,一个TCP连接下可以发送6个http请求,这样的话就可以设置多个域名来达到同时发送多个http请求的目的。
  2. 还可以使用浏览器缓存策略对已经请求过的但未发生改变的数据进行缓存,这样可以减少http请求的次数。

从而达到加速效果,这个可以优化 FP、FCP、LCP 指标。

lighthouse还给了建议去优化耗时任务,

  1. 用 Web Worker 将耗时任务丢到子线程中—-10w条数据 pre
  2. 动画使用RAF
  3. 希望在浏览器的空闲时间去执行而不卡顿主线程的事件可用RIC去执行
  4. 大数据渲染使用虚拟列表
  5. 大文件上传使用分片上传

这些可以优化
TTI(页面开始到子资源响应用户输入的时间)、
FID( 首次输入延迟时间 — 衡量网站互动顺畅程度)、
TBT (阻塞总时间)指标

还建议不要动态插入内容

  1. 要给图片设置长宽,给一个图片位置的预期
  2. 如果需要在某些内容之间插入内容的话,给出一个预留位置
  3. 避免重绘重排,比如:多次dom操作可以先放进DocumentFragment,最后更新完后一次性渲染到真实dom上;尽量不要操作offsetTop, offsetLeft, offsetWidth等等这些会引起重排;动画启用GPPU加速(css3动画)等等

这些可以优化 CLS 指标

我还在代码层面上做了优化
(1)合理使用v-if和v-show
(2)合理使用watch和computed
(3)使用v-for必须添加key, 最好为唯一id,把v-for和v-if分开使用