性能优化

性能优化

1. 从输入URL到页面加载完成,发生了什么

2. webpack与Gzip

3. 图片优化

4. 浏览器缓存

5. 本地存储

6. CDN

7. 服务端渲染

8. 浏览器端的性能优化

9. DOM优化

10. 事件循环与异步更新策略

11. 回流与重绘

12. 优化首屏体验-懒加载

13. 节流与防抖

14. 性能监测


从输入URL到页面加载完成,发生了什么

  1. 用户输入URL

  2. 浏览器解析URL

  • 浏览器内部代码会解析这个URL。它首先会检查本地hosts文件,看是否有对应的域名。如果有,浏览器就会直接向该IP地址发送请求。如果没有,浏览器会将域名发送给DNS服务器进行解析,将域名转换成对应的服务器IP地址。
  1. 建立TCP连接(三次握手)
  • 浏览器得到IP地址后,会通过TCP协议与服务器建立连接。TCP/IP协议是Internet的基础,它负责确保数据在网络中的可靠传输。这一过程中会进行三次握手,确保双方都已准备好进行通信。
  1. 发送HTTP请求
  • TCP连接建立后,浏览器会向服务器发送HTTP请求。这个请求包含了请求行(如GET方法、请求的URI、HTTP版本等)、请求头部(如Accept-Charset、Accept-Encoding等)以及可能存在的请求正文。
  1. 服务器处理请求
  • 服务器收到请求后,会根据请求的内容进行相应的处理。这可能包括查询数据库、生成动态内容等。
  1. 发送HTTP响应
  • 服务器处理完请求后,会发送一个HTTP响应给浏览器。这个响应包含了状态行(如HTTP版本、状态码、状态描述等)、响应头部(如Content-Type、Content-Length等)以及响应正文(即实际要显示的页面内容)。
  1. 浏览器解析和渲染页面
  • 浏览器收到响应后,会解析响应正文中的HTML代码,并下载所需的CSS、JavaScript等资源文件。然后,浏览器会根据这些资源来渲染页面,最终将页面呈现给用户。
  1. TCP连接关闭(四次挥手)
  • 浏览器解析渲染页面
  • 解析文档,生成DOM树;
  • 解析css,根据css规则生成CSSOM规则树;
  • 在CSSOM树和DOM树生成完后,合并DOM、CSSOM树构建渲染树;
  • 渲染树构建完成后,开始计算元素大小和位置;
  • 根据计算好的位置信息将内容渲染到屏幕上;
  • 通过URL页面加载考虑性能优化的点,如网络层面的优化可考虑DNS解析使用DNS缓存,TCP连接使用长连接,HTTP请求优化减少请求次数和请求体积,请求最近服务器上的资源;浏览器端的优化可考虑资源加载优化、服务端渲染、浏览器缓存机制的利用、DOM 树的构建、网页排版和渲染过程、回流与重绘的考量、DOM 操作的合理规避等;

webpack与Gzip

  • 从URL到显示页面这个过程,涉及到网络层面的有3个主要过程:DNS解析、TCP连接、HTTP请求/响应;对于DNS解析和TCP连接两个步骤,前端可以做的努力有限。HTTP连接层面的优化是网络优化的核心。
  • HTTP优化的两个大的方向:减少请求次数、减少单次请求所花费的时间;考虑资源的压缩与合并,也是构建工具在做的事情,如webpack;
  • webpack性能优化的瓶颈:webpack的构建过程太花时间;webpack打包的结果体积太大;
webpack优化方案
  1. 构建过程提速
  • 减少loader事情,如include或exclude避免不必要的转译;开启缓存将转译结果缓存至文件系统,cacheDirectory=true;
  • 不要放过第三方库,以node_modules为代表,文件很大又不可或缺;处理第三方库很很多种方式,如DllPlugin只有当依赖自身发生版本变化时才会重新打包;
  • 将loader由单进程转为多进程;HappyPack、thead-loader;
  1. 构建结果体积压缩
  • 移除 console;
  • 拆分资源;
  • 删除冗余代码,如Tree-Shaking,UglifyJsPlugin,适合用来处理模块级别的冗余代码;至于粒度更细的冗余代码的去除,往往会被整合进js或css的压缩或分离过程中;
  • 按需加载,require.ensure在正确的时机去触发相应的回调;
  • 多线程压缩TerserPlugin
  1. Gzip压缩:在一个文本文件中找出一些重复出现的字符串,临时替换它们,从而使整个文件变小;
  • 开启Gzip只需要在request headers中加一句话:accept-encoding:gzip
  • HTTP压缩,以缩小体积为目的,对HTTP内容进行重新编码的过程;

图片优化

  1. JPEG、JPG,
  • 有损压缩、体积小、加载快,不支持透明;
  • 大的背景图、轮播图或Banner图出现;使用 JPG 呈现大图,既可以保住图片的质量,又不会带来令人头疼的图片体积;
  • 当它处理矢量图形和 Logo 等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显;
  • JPEG 图像不支持透明度处理;
  1. PNG-8/PNG-24,
  • 无损压缩、质量高、体积大、支持透明;
  • 呈现小的 Logo、颜色简单且对比强烈的图片或背景等;
  1. SVG,
  • 文本文件、体积小、不失真、兼容性好;
  • SVG 对图像的处理不是基于像素点,而是是基于对图像的形状描述,是一种基于XML语法的图像格式;
  • 局限性:渲染成本比较高,不利于性能;可编程的,学习成本大;
<!-- 将svg写入html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <svg xmlns="http://www.w3.org/2000/svg"   width="200" height="200">
        <circle cx="50" cy="50" r="50" />
    </svg>
</body>
</html>

<!-- 将svg写入独立文件后引入html -->
<img src="文件名.svg" alt="">
  1. Base64,
  • 文本文件、依赖编码、小图标的解决方案;
  • Base64 和雪碧图,是作为小图标解决方案而存在的;相较于一个小图标一个图像文件,单独一张图片所需的 HTTP 请求更少,对内存和带宽更加友好。
  • Base64 是作为雪碧图的补充而存在的;
  • Base64 是一种用于传输 8Bit 字节码的编码方式,通过对图片进行 Base64 编码,可直接将编码结果写入 HTML 或者写入 CSS,减少 HTTP 请求的次数。
  • 缺陷:base64编码之后,图片大小会膨胀为原来的4/3,所以不适合大背景图片;
  • 使用base64:图片的实际尺寸很小;图片无法以雪碧图的形式与其他小图结合(合成雪碧图仍是主要的减少http请求的途径,base64是雪碧图的补充);图片的更新频率非常低;
  • 利用 webpack 来进行 Base64 的编码,url-loader,具备base64转码能力,还可以结合文件大小,判断图片是否有必要进行base64编码;
  1. WebP,
  • 年轻的全能型选手;像 JPEG 一样对细节丰富的图片信手拈来,像 PNG 一样支持透明,像 GIF 一样可以显示动态图片——它集多种图片文件格式的优点于一身;
  • 加快图片加载速度的图片格式,支持有损压缩和无损压缩;
  • 缺点:年轻,浏览器兼容性问题;
<!-- 维护性更强、更加灵活的方案——把判断工作交给后端,由服务器根据 HTTP 请求头部的 Accept 字段来决定返回什么格式的图片。当 Accept 字段包含 image/webp 时,就返回 WebP 格式的图片,否则返回原图 -->
<img src="//img.alicdn.com/tps/i4/TB1CKSgIpXXXXccXXXX07tlTXXX-200-200.png_60x60.jpg_.webp" alt="手机app - 聚划算" class="app-icon">


浏览器缓存

  • 缓存可以减少网络 IO 消耗,提高访问速度。
  • 浏览器缓存按照获取资源时请求的优先级顺序分为:Memory Cache、Service Worker Cache、HTTP Cache、Push Cache,可以从请求的network的Size一栏看出是哪种缓存;
HTTP缓存:强缓存、协商缓存;
  1. 强缓存
  • 利用http头中的Expires和Cache-Control(max-age/s-maxage)两个字段来控制的。
  • 强缓存中,当请求再次发出时,浏览器会根据其中的expires和cache-control判断目标资源是否命中缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信;命中强缓存返回200(disk cache);
  • Cache-Control 的 max-age 配置项相对于 expires 的优先级更高。当 Cache-Control 与 expires 同时出现时,以 Cache-Control 为准。cache-control: max-age=3600, s-maxage=31536000 s-maxage 优先级高于 max-age,两者同时出现时,优先考虑 s-maxage(代理服务器的缓存问题)。如果 s-maxage 未过期,则向代理服务器请求其缓存内容。
  1. 协商缓存:
  • 依赖于服务端与浏览器之间的通信;
  • 协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。
  • 如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304;
  • Last-Modified、if-None-Match,存在不能正确感知文件变化的情况,常见的两个场景:(1) 我们编辑了文件,但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求;(2)当我们修改文件的速度过快时(比如花了 100ms 完成了改动),由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,所以它是感知不到这个改动的——该重新请求的时候,反而没有重新请求了;
  • Etag,由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,能够精准地感知文件的变化;需要服务器额外付出开销,会影响服务端的性能;
  1. HTTP缓存决策
  • 首先,当资源内容不可复用时,Cache-Control设置为no-strore.拒绝缓存;
  • 其次,考虑是否需要每次向服务器进行缓存有效确认,需要则设置Cache-Control为no-cache,不需要确认则考虑该资源是否可以被代理服务器缓存,根据其结果决定是设置为private还是public;
  • 然后,考虑该资源的过期时间,设置对应的max-age和s-maxage值;
  • 最后设置协商缓存需要用到的Etag、Last-Modified等参数;
MemoryCache:存在内存中的缓存;
  • 存在内存中的缓存;
  • 从优先级上来说,它是浏览器最先尝试去命中的一种缓存;从效率上来说,它是响应速度最快的一种缓存;
  • Base64 格式的图片,几乎永远可以被塞进 memory cache;
  • 当进程结束后,即tab关闭后,内存里的数据也不复存在;
Service Worker Cache
  • 一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM;
  • Service Worker可以帮助我们实现离线缓存、消息推送、网络代理等功能;借助Service worker实现的离线缓存被称为Service Worker Cache;
  • Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它。这是它可以用来实现离线存储的重要先决条件。
  • Server Worker 对协议是有要求的,必须以 https 协议为前提
Push Cache
  • Push Cache 是指 HTTP2 在 server push 阶段存在的缓存;
  • Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
  • Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
  • 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。

本地存储

  • Cookie、Web Storage、IndexedDB
  1. Cookie
  • 有体积上限的,它最大只能有 4KB;
  • 通过响应头里的 Set-Cookie 指定要存储的 Cookie 值。默认情况下,domain 被设置为设置 Cookie 页面的主机名,我们也可以手动设置 domain 的值:Set-Cookie: name=xiuyan; domain=xiuyan.me; 同一个域名下的所有请求,都会携带 Cookie;
  1. Web Storage:
  • 为浏览器存储而提供的数据存储机制, Local Storage 与 Session Storage,二者的区别在于生命周期和作用域的不同;
  • 存储容量大, 5-10M;
  • 仅位于浏览器端,不与服务端发生通信;
  • Local Storage:持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;适合存储一些内容稳定的资源,如bse64图片字符串、不经常更新的css、js静态资源;
  • Session Storage:Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便无法共享。适合用来存储生命周期和它同步的会话级别的信息;
// 存储数据
localStorage.setItem('user_name', 'aa')

// 读取数据
localStorage.getItem('user_name')

// 删除某一键值数据
localStorage.removeItem('user_name')

// 清空数据记录
localStorage.clear()
  1. IndexedDB
  • 一个运行在浏览器上的非关系型数据库;

CDN

  • CDN 往往被用来存放静态资源 (像 JS、CSS、图片等不需要业务服务器进行计算即得的资源);静态资源访问频率高、承接流量大,因此静态资源加载速度始终是前端性能的一个非常关键的指标。CDN是静态资源提速的重要手段。
  • CDN核心点:缓存、回溯;
  • 缓存:把资源copy一份到CDN服务器上的过程;
  • 回溯:CDN发现自己没有需要的资源(一般是缓存过期),向它的上层服务器/根服务器去要该资源的过程;
  • 把静态资源和主页面置于不同的域名下,完美地避免了不必要的 Cookie 的出现!

服务端渲染SSR

  • 本质:本该浏览器做的事情,分担给服务器去做;
  • 是什么 服务端渲染的运行机制
    1. 客户端渲染模式:服务端会把渲染需要的静态文件发给客户端,客户端加载过来之后,自己在浏览器里跑一遍JS,根据JS的运行结果,生成相应的DOM;这样页面上呈现的内容,在html源文件里找不到;
    1. 服务端渲染模式:当用户第一次请求页面时,由服务器把需要的组件或页面渲染成HTML字符串,然后把它返回给客户端;客户端拿到手的是可以直接渲染然后呈现给用户的HTML内容,不需要为了生成DOM再跑一遍JS;使用服务端渲染即所见即所得,页面上呈现的内容,我们在html源码文件里也能找到。
  • 为什么 服务端渲染解决了什么性能问题
  • 使用服务端渲染之后,利于搜索引擎搜索出关键词;且解决了首屏加载慢的问题;(客户端渲染模式下,除了加载HTML,还要等渲染所需的JS加载完,还要把这部分JS在浏览器上再跑一遍)
  • 怎么做 服务端渲染的应用实例与使用场景
    1. renderToString() 将虚拟DOM转化为真实DOM;
    1. 把转化结果塞进模板里;
// vue,服务端入口文件
const Vue = require('vue')
// 创建一个express应用
const server = require('express')()
// 提取出renderer实例
const renderer = require('vue-server-renderer').createRenderer()

server.get('*', (req, res) => {
  // 编写Vue实例(虚拟DOM节点)
  const app = new Vue({
    data: {
      url: req.url
    },
    // 编写模板HTML的内容
    template: `<div>访问的 URL 是: {{ url }}</div>`
  })
    
  // renderToString 是把Vue实例转化为真实DOM的关键方法
  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    // 把渲染出来的真实DOM字符串插入HTML模板中
    res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>
    `)
  })
})

server.listen(8080)

浏览器端的性能优化

  • 浏览器内核可以分成两部分:渲染引擎、JS引擎;
  • 渲染引擎包括:HTML解释器、CSS解释器、布局、网络、存储、图形、音视频、图片解码器等零部件;
  • 浏览器内核分类:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)
  • Blink基于 Webkit 衍生而来的一个分支
  • DOM树:接续HTML以创建的是DOM树,渲染引擎开始解析HTML文档,转换树转换的标签到DOM节点,它被称为DOM树;
  • CSSOM树:解析css创建的是CSSOM树,CSSOM的解析过程与DOM树的解析过程是并行的;
  • 渲染树:CSSOM与DOM结合,得到的是渲染树;
  • 布局渲染树:从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标,便得到了基于渲染树的布局渲染树;
  • 绘制渲染树:遍历渲染树,每个节点将使用UI后端层来绘制,整个过程叫做绘制渲染树;
  • 渲染过程:首先基于HTML构建一个DOM树,这棵DOM树与CSS解释器解析出的CSSOM相结合,就有了布局渲染树。最后浏览器以布局渲染树为蓝本,去计算布局并绘制图像,我们页面的初次渲染就完成了。之后每当一个新元素加入到这个DOM树当中,浏览器便会通过CSS引擎查遍CSS样式表,找到符合该元素的样式规则应用到这个元素上,然后再重新去绘制它。
  • 查表是个花时间的活,怎么让浏览器的查询工作又快又好地实现呢?第一个可转化为代码的优化点——CSS 样式表规则的优化.
CSS 样式表规则的优化
  1. 样式查找:CSS引擎查找样式表,每条规则都是从右向左匹配,#mylist li {} 这样会先匹配到li,查找所有li,再对li父元素的id进行匹配,这样li过多的情况下就会造成性能浪费;* {}通配符会匹配所有元素;基于此,可考虑:(1)避免使用通配符,只对需要用到的元素进行选择; (2)关注可以通过继承实现的属性,避免重复匹配重复定义; (3)少用标签选择器,如果可以,可以用类代替;(4)不要画蛇添足,id和class选择器不应该被多余的标签选择器拖后腿,如.myList#title,可#title。(5)减少嵌套,后代选择器的开销是最高的,尽量将选择器的深度降到最低,尽量不要超过三层,尽可能使用类来关联每一个标签元素;

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

  • CSS阻塞:将 CSS 放在 head 标签里;启用 CDN 实现静态资源加载速度的优化;
  • JS阻塞:通过对js使用 defer 和 async 来避免不必要的阻塞;
  • JS的三种加载方式:
  • 正常模式<script src="index.js"></script>这种情况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去做其它事情。
  • async模式<script async src="index.js"></script>async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。
  • defer模式<script defer src="index.js"></script>defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。

DOM优化

  1. DOM优化思路:减少 DOM 操作,一方面是dom渲染影响性能,另一方面就是js引擎和渲染引擎交流需要成本;减少 DOM 操作的核心思路,就是让 JS 去给 DOM 分压
  2. 异步更新策略
  3. 回流与重绘
  • 对DOM的修改触发了渲染树的变化,就会触发回流或重绘。
  • 回流:当对DOM的修改引发DOM几何尺寸的变化如修改元素的宽、宽、位置、隐藏元素时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来。这个过程叫回流,也叫重排。
  • 重绘:当对DOM的修改导致了样式的变化,但并未影响其几何属性,如修改颜色或背景色,浏览器不需要重新计算元素的几何属性,直接为该元素绘制新的样式。这个过程叫重绘。
  • 重绘不一定会导致回流;回流一定会导致重绘;
// 每一次循环都调用 DOM 接口重新获取了一次 container 元素,相当于每次循环都交了一次过路费。前后交了 10000 次过路费,但其中 9999 次过路费都可以用缓存变量的方式节省下来
for(var count=0;count<10000;count++){ 
  document.getElementById('container').innerHTML+='<span>我是一个小测试</span>'
} 

// 缓存变量
// 只获取一次container
let container = document.getElementById('container')
for(let count=0;count<10000;count++){ 
  container.innerHTML += '<span>我是一个小测试</span>'
} 

// 上面代码不必要的 DOM 更改太多了
let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){ 
  // 先对内容进行操作
  content += '<span>我是一个小测试</span>'
} 
// 内容处理好了,最后再触发DOM的更改
container.innerHTML = content


// 字符串变量 content 就扮演着一个 DOM Fragment 的角色。其实无论字符串变量也好,DOM Fragment 也罢,它们本质上都作为脱离了真实 DOM 树的容器出现,用于缓存批量化的 DOM 操作。
let container = document.getElementById('container')
// 创建一个DOM Fragment对象作为容器
let content = document.createDocumentFragment()
for(let count=0;count<10000;count++){
  // span此时可以通过DOM API去创建
  let oSpan = document.createElement("span")
  oSpan.innerHTML = '我是一个小测试'
  // 像操作真实DOM一样操作DOM Fragment对象
  content.appendChild(oSpan)
}
// 内容处理好了,最后再触发真实DOM的更改
container.appendChild(content)

事件循环与异步更新策略

浏览器中的事件循环
  • JS分为同步任务和异步任务;
  • 同步任务都在主线程(JS引擎线程)上执行,会形成一个执行栈;主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调;一旦执行栈中的所有同步任务执行完毕(JS引擎线程空闲了),系统就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中,开始执行;
  • 事件循环中的异步队列有2种:宏任务队列、微任务队列
  • 常见的宏任务:主代码块、setTimeout、setInterval、setImmediate、script整体代码、I/O操作、requestAnimationFrame 、UI渲染等;
  • 常见的微任务: process.nextTick、Promise.then、catch、finally、Object.observe、MutationObserver 等;
  • 浏览器会先执行一个宏任务,紧接着执行当前执行栈产生的微任务,再进行渲染,然后再执行下一个宏任务;
  • 微任务和宏任务不在一个任务队列;如setTimeout是一个宏任务,它的事件回调在宏任务队列,Promise.then()是一个微任务,它的事件回调在微任务队列;
  • 一个完整的事件循环过程:
  • 首先,整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务、异步任务两部分;
  • 同步任务会直接进入主线程依次执行;
  • 异步任务会再分为宏任务和微任务;
  • 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
  • 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
  • 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;
  • 上述过程会不断重复,这就是Event Loop,比较完整的事件循环;
  • 每一个循环都是这样一个过程:将一个macro-task执行并出队=》将一队micro-task执行并出队=》执行渲染操作,更新界面=》处理下一个macro-task;
  • 更新 DOM 的时间点,尽可能靠近渲染的时机。当需要在异步任务中实现 DOM 修改时,把它包装成 micro 任务是相对明智的选择。Promise.resolve().then(task)
  • 异步更新:更新并不会立即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发,这就是异步更新。异步更新可以帮助我们避免过度渲染。只看结果。

回流与重绘

  • 回流:当对DOM的修改引发了DOM几何尺寸(如修改元素的宽高位置或隐藏元素)的变化时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来,这个过程叫回流。
  • 重绘:当对DOM的修改导致了样式的变化,却并未影响其几何属性时,如修改颜色背景色,浏览器不需要重新计算元素的几何属性,直接为该元素绘制新的样式,这个过程叫重绘。
  • 重绘不一定会导致回流,回流一定会导致重绘。
  • 要避免回流与重绘的发生,最直接的做法是避免掉可能会引发回流与重绘的 DOM 操作;
  • 触发重绘的操作:背景色、文字色、可见性(这里指visibility:hidden这样不用=改变元素位置和存在性的,单纯针对可见性的操作)
  • 触发回流的操作:改变DOM元素的几何属性(width/height/padding/margin/left/top/border等);改变DOM树的结构(增减、移动等操作);获取一些特定属性(offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight)的值,这些值有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流、当调用了 getComputedStyle 方法,或者 IE 里的 currentStyle 时,也会触发回流。
  • 如何规避回流与重绘:
    1. 将敏感操作缓存起来,避免频繁改动
  // 获取el元素
  const el = document.getElementById('el')
  // 这里循环判定比较简单,实际中或许会拓展出比较复杂的判定需求
  for(let i=0;i<10;i++) {
      el.style.top  = el.offsetTop  + 10 + "px";
      el.style.left = el.offsetLeft + 10 + "px";
  }

// 改成
// 缓存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. 避免逐条改变样式,使用类名去合并样式
const container = document.getElementById('container')
// 每次单独操作,都去触发一次渲染树更改,从而导致相应的回流与重绘过程。
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

// 改成
 .basic_style {
    width: 100px;
    height: 200px;
    border: 10px solid red;
    color: red;
  }
const container = document.getElementById('container')
// 将所有的更改一次性发出,用一个 style 请求解决掉了
container.classList.add('basic_style')
    1. 将DOM离线,将元素设置为display:none,将其从页面上拿掉,后续再操作将无法触发回流与重绘;
// DOM离线前
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...

// DOM离线后
let container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...
container.style.display = 'block'
  • flush队列
  • 下面代码在现代浏览器中只进行了一次回流和一次重绘,和上面的知识点有出入,原因在于:浏览器自己缓存了一个flush队列,把触发的回流和重绘任务都塞进去,待JavaScript执行栈清空时或者不得已时将这些任务一次性出队。
  • 不得已的时机:获取一些特定属性(offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight)的值,这些值有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流;
let container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

优化首屏体验-懒加载

  • 懒加载:主要用于图片、视频或其他大型资源的加载;原理是:当用户滚动页面到特定位置时再去加载相应的资源,而不是在页面初始加载时就一次性加载所有资源。
  • 图片懒加载,针对图片加载时机的优化,通常是通过监听滚动事件或使用 Intersection Observer API 来判断图片是否进入了可视区域,如果进入则加载图片。这样可以减少初始页面加载时间,提高页面加载速度,节省带宽,提升用户体验。
  • 在视频方面,懒加载可以避免在页面加载时就预加载整个视频,而是在用户点击播放或视频即将进入可视区域时再加载。
  • 在懒加载的实现中,有两个关键的数值:一个是当前可视区域的高度const viewHeight = window.innerHeight || document.documentElement.clientHeight;一个是元素距离可视区域顶部的高度getBoundingClient().top
    1. 给需要懒加载的图片设置一个自定义属性,例如 data-src 来存储图片的真实 URL,而初始的 src 属性可以设置为一个占位图片或空白。
    1. 在页面加载时,获取所有需要懒加载的图片元素。
    1. 为滚动事件添加监听函数。
    1. 在监听函数中,判断图片是否进入了可视区域。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Lazy-Load</title>
  <style>
    .img {
      width: 200px;
      height:200px;
      background-color: gray;
    }
    .pic {
      // 必要的img样式
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="img">
      <!-- 注意我们并没有为它引入真实的src -->
      <!-- data-src 来存储图片的真实 URL,而初始的 src 属性可以设置为一个占位图片或空白 -->
      <img class="pic" alt="加载中" data-src="./images/1.png">
    </div>
    <div class="img">
      <img class="pic" alt="加载中" data-src="./images/2.png">
    </div>
    <div class="img">
      <img class="pic" alt="加载中" data-src="./images/3.png">
    </div>
    <div class="img">
      <img class="pic" alt="加载中" data-src="./images/4.png">
    </div>
    <div class="img">
      <img class="pic" alt="加载中" data-src="./images/5.png">
    </div>
     <div class="img">
      <img class="pic" alt="加载中" data-src="./images/6.png">
    </div>
     <div class="img">
      <img class="pic" alt="加载中" data-src="./images/7.png">
    </div>
     <div class="img">
      <img class="pic" alt="加载中" data-src="./images/8.png">
    </div>
     <div class="img">
      <img class="pic" alt="加载中" data-src="./images/9.png">
    </div>
     <div class="img">
      <img class="pic" alt="加载中" data-src="./images/10.png">
    </div>
  </div>
</body>
</html>


// 获取所有的图片标签
const imgs = document.getElementsByTagName('img');

// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight;

// num用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
let num = 0;
function lazyload() {
  for (let i = num; i < imgs.length; i++) {
    // 用可视区域高度减去元素顶部距离可视区域顶部的高度
    let distance = viewHeight - imgs[i].getBoundingsClientRect().top;
    // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
    if (distance >= 0) {
      // 给元素写入真实的src,展示图片
      imgs[i].src = imgs[i].getAttribute('data-src');
      // 前i张图片加载完毕,下次从第i+1张开始检查是否露出
      num = i + 1;
    }
  }
}

// 监听scroll事件,false 参数表示事件处理函数在冒泡阶段执行
window.addEventListener('scroll', lazyload, false);

// 用户的每一次滚动都将触发我们的监听函数。函数执行是吃性能的,频繁地响应某个事件将造成大量不必要的页面计算。针对那些有可能被频繁触发的事件作进一步地优化——throttle 与 debounce。


节流与防抖

  • 类似scroll事件、rsize事件、鼠标事件如mousemove/mouseover、键盘事件如keyup/keydown,都存在被频繁触发的风险;
  • 频繁触发回调导致的大量计算会引发页面的抖动甚至卡顿,需要一些手段来控制事件被触发的频率,由此出现了事件节流throttle和事件防抖debounce;
  • 节流与防抖的本质:都以闭包形式存在;通过对事件对应的回调函数进行包裹,以自由变量的形式缓存时间信息,最后用setTimeout来控制事件的触发频率;
  • 节流throttle:第一个人说了算;在某段事件内,不管你触发了多少次回调,都只认第一次,并在计时结束时给与响应;
// fn是我们需要包装的事件回调,interval是时间间隔的阈值
function throttle(fn, interval) {
  // last为上一次触发回调的时间
  let last = 0;

  // 将throttle处理结果当作按时返回
  return function () {
    // 保留调用时的this上下文
    let context = this;
    // 保留调用时传入的参数
    let args = arguments;
    // 记录本次触发回调的时间
    let now = +new Date();

    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last >= interval) {
      // 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
      last = now;
      fn.apply(context, args);
    }
  }
}

// 使用throttle来包装scoll的回调
const better_scroll = throttle(() => console.log('scroll---'), 1000);

document.addEventListener('scroll', better_scroll);
  • debounce:最后一个人说了算;在某段时间内,不管你触发了多少次回调,都只认最后一次;
  • 如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。
// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
    // 定时器
  let timer = null;
  // 将debounce处理结果当作函数返回
  return function () {
      // 保留调用时的this上下文
    let context = this;
    // 保留调用时传入的参数
    let args = arguments;
     // 每次事件被触发时,都去清除之前的旧定时器
    if (timer) {
      clearTimeout(timer)
    }
        // 设立新定时器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

const better_scroll = debounce(() => console.log('scroll--'), 1000);
document.addEventListener('scroll', better_scroll);
  • 在throttle的逻辑里,“第一个人说了算”,它只为第一个乘客计时,时间到了就执行回调。而 debounce 认为,“最后一个人说了算”,debounce 会为每一个新乘客设定新的定时器。
  • 用throttle优化debounce
// fn是需要包装的事件回调,delay是时间间隔的阈值
function throttle(fn, delay) {
  // last为上一次触发回调的事件,timer是定时器
  let last = 0, timer = null;
  // 将throttle处理结果当作函数返回
  return function () {
    // 保留调用时的this上下文
    let context = this;
    // 保留调用时传入的参数
    let args = arguments;
    // 记录本次触发回调的时间
    let now = +new Date();

    // 判断上次触发的时间额本次触发的时间差是否小于时间间隔的阈值
    if (now - last < delay) {
      // 如果时间间隔小于设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
      clearTimeout(timer);
      timer = setTimeout(function () {
        last = now;
        fn.apply(context, args)
      }, delay)
    } else {
      // 如果时间间隔超出了设定的时间间隔阈值,那就直接执行一次反馈给用户
      last = now;
      fn.apply(context, args)
    }
  }
}

性能监测

  • 监测的目的:确定性能瓶颈,优化;
  • 性能监测方案:可视化方案、可编程方案
  • Performance,Chrome 开发者工具,用于记录和分析应用在运行时的所有活动。它呈现的数据具有实时性、多维度的特点,可以定位性能问题。
  • 可视化监测:Lighthouse,chrome插件;可生成性能报告;
  • 可编程的性能上报: window.performance






参考&感谢各路大神

前端性能优化原理与实践

JS运行机制

HTTP及浏览器篇

posted @ 2024-07-24 17:32  安静的嘶吼  阅读(9)  评论(0编辑  收藏  举报