前端性能优化实战

92 阅读12分钟

前端性能优化大概可以从一道面试题说起: 从输入URL到页面加载完成发生了什么?

  • 首先我们需要通过DNS将URL解析为对应的IP地址,然后与这个IP地址确定的那台服务器发生TCP链接,随后我们向服务器抛出我们的HTTP请求,服务端处理完后我们的请求,把目标数据放在HTTP响应里返回给客户端,拿到响应数据的浏览器就可以开始走一个渲染的流程。渲染完毕后呈现给用户。
  • 网络传输图片

网络层面

  1. DNS解析
  2. TCP连接
  3. HTTP请求发出
  4. 服务端处理请求, HTTP响应返回
  5. 浏览器拿到响应数据,解析响应数据,把解析的额结果展现给用户

1. webpack性能优化实践

在webpack中以减少请求次数, 减少单次请求所花费的时间为目标,通常webpack中常见操作

  1. 资源合并
  2. 资源压缩
  • HTTP 的优化的常见操作: 资源压缩与合并,用压缩工具所做的事情(webpack)
  • webpack的性能瓶颈:
    1. webpack 的构建过程太花时间
    2. webpack 的打包体积太大
  • 提升构建速度的方式
    1. 构建过程策略, 减少loader的范围, 使用include/exclude`进行减少不必要转义文件
    2. 第三方库的优化
      • UI库按需加载策略

      • element-plus

      • webpack-bundle-analyzer包体积分析

      • 语言库的按需加载

        • moment
      • 按需加载分包管理 (webpackChunk, defieneAsyncComponent),底层实现都是逻辑都是沟通过webpack中按需记载的方式,打包出不同包名的方案。

      • 使用 externals 配合 CDN 进行使用

      • DLLPlugin(动态链接库): 把第三方库单独打包一个文件中,不随着业务代码重启打包,只有依赖的自身发生变化的时候才重新打包

        •  const path = require('path')
           const webpack = require("webpack")
           module.exports = {
                entry: {
                   vendor: "vue vue-router babel-polyfill element-plus axios pinia"
                },
                output: {
                    path: path.resolve(__dirname, "dist"),
                    filename: "[name].js",
                    library: "[name]_[hash]"
                },
                plugins: [
                // 打包出动态链接库的文件
                    new webpack.DLLPlugin({
                        //DLLPlugin的name属性和library保持一致
                        name: "[name]_[hash]",
                        path: path.join(__dirname, "dist", "[name]-manifest.json"),
                        // context需要和webpack.config.js保持一致
                        context: __dirname
                    })
                ]
           }
          
        • 打包生成vendor.js 和 vendor.manifest.json文件
          • manifest
          • 描述了第三方库对应的具体路径
        • 使用生成的动态链接文件
            {
             ... 
                plugins: [
                   new webpack.DLLReferencePlugin({
                     context: __dirname,
                     manifest: require("./dist/vendor-manifest.json")
                   })
                ]
             ...
            }
          
      1. Happypack-将loader单进程转化为多进程,是把loader的配置转移到Happypack配置的并发进程中, 开启操作系统的并发进程数
         const HappyPack = require("happypack");
         const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length})
         
         module.exports = {
             module: {
                rules: [{
                  test: /\.js$/,
                  // 问号后面的查询参数指定处理这个文件的happypack实例的名字
                  loader: "happypack/loader?id=happyBabel",
                }]
             },
             plugins: [
               new HappyPack({
                 id: "happyBabel",
                 threadPool: happyThreadPool,
                 loaders: ["babel-loader?cacheDirectory"]
               
               })
             ]
         }
      
      1. 拆分资源,删除冗余的代码
        • tree-shaking删除静态分析无用的代码 es6Module和commonjs的区别?
        • 更细粒度的js/css压缩文件,通过webpack中的optimization去配置
      2. Gzip压缩(重复压缩)

2. 图片优化:图片体积和质量的平衡点

图片的格式

  1. jpeg/Jpg: 有损压缩 体积小 加载快 不支持透明

    • 场景:JPG 适合呈现色彩丰富的图片, JPG 经常作为背景图, 轮播图和 banner 图出现

    JPG 格式以 24 为存储单个图,决定了前后压缩的质量损耗不容易被察觉, 使用 JPG 呈现大图, 即保住了质量,又减小了体积 JPG 在处理矢量图和 logo 等线条等压缩会变得很模糊,此外 JPG 不支持透明处理.

  2. PNG8 与 PNG24:无损压缩 质量高,体积大 支持透明

    • 场景:应用 logo, 颜色简单对比强烈的小图, 背景等

    png 图片色彩表现力更强,多线条处理更加细腻 对透明度支持良好,问题就是体积太大

  3. SVG:文件体积更小, 压缩性更强

    • 场景:logo和图标 > 图片无限方法而不失真,1 张 svg 可以适配 n 种分辨率, 但是 svg 渲染成本比较高
  4. Base64: 文本文件 依赖编码 小图标解决问题 - 和雪碧图一样 base64 图片的出现, 为减少了网页图片对服务器的请求。提升性能 - base64 图片会增大原来的 1/3, 大图片体积过大和减少请求比起来得不偿失

    • 场景
      1. 图片的尺寸非常小
      2. 图片无法以雪碧图与其他小图结合
      3. 图片的更新频率非常低
  5. webP

    • 一种加快图片加载速度的图片格式 它支持有损压缩和无损压缩
    • 兼容性不是很好
    • webp 会增加服务器的负担,编写同质量的 webP 文件,消耗更多的计算资源

3.存储和缓存

缓存可以减少 IO 消耗, 重复利用之前获取的资源能力,提高访问速度

  1. MemoryCache: base64 小体积的 css 和 js
  2. Service Worker Cache: 实现离线缓存, 消息推送和网络代理等功能, 分为 install active working 三个阶段,只要安装就始终存在
  3. HTTP Cache
  4. Push Cache
  • http 缓存的机制
    1. 强缓存
      • expires: response headers 中写入 expires 字段,是一个时间戳

        缺点: 客户端与服务器时间要保证高度一致性

      • cache-control
        • max-age 设定相对时间, 在当前时间中生效

          屏蔽服务端, 客户端会记录请求到资源点,作为起点, 其实时间和当前时间都来来自客户端 cache-control 的 max-age 配置相对 expires 优先级更高.

        • s-maxage: 比 max-age 优先级更高, 仅在代理服务器请求缓存内容, 并只对 public 缓存有效, 客户端我们只考虑 max-age
        • no-cache: 每次发起请求不会走浏览器缓存,直接向服务器确认资源是否过期
        • no-store: 不适用任何缓存, 直接向服务端发送信息
      • 根据这两个字段哦安短命中资源, 若命中直接返回缓存资源, 不会与服务器发生通信
    2. 协商缓存

      优先级较高的强缓存, 在命中强缓存失败的情况下, 才走协商缓存

      • 依赖于浏览器和服务器的通信,协商缓存机制下, 浏览器向服务器询问缓存的资源信息, 进而判断是否要重新发送请求,还是从本地获取数据, 如果服务端提示缓存未改动,资源会被重定向到浏览器缓存,这种情况下状态码是 304
      • 协商缓存实现: 从 last-modified 到 etag

        last-modify 是一个时间戳,会首次在 response headers 返回,随后我们再次请求会带上 if-modified-since 字段, 他的值是 response 返回的 last-modified 的值, 会对比时间戳和资源服务器最后修改时间 场景:1. 当我们修改了文件重新修改回去内容没发生变化但是时间发生了变化,资源没变,但是会重新发送请求, 2,就是较短的时间内修改了内容,毫秒级变化

      • etag: 每个资源生成的唯一标识字符串。基于文件内容的,采用的是摘要算法,当下次请求的时候带上 if-none-match 字段
  • 本地存储
    • cookie: 存储在浏览器中的文本文件,每次在 http 请求上传递给服务器,携带用户信息,当服务器检测 cookie 的时候就可以获取客户端用户状态
      1. cookie: 最大只有 48kb, cookie 只能获取少量的信息
      2. 通过 set-cookie 存储的 cookie 的值,默认情况下 cookie 设置为 cookie 页面的主机名
      3. 同一个域名下,所有请求都会携带 cookie
    • localStorage 和 sessionStorage
      1. 生命周期和作用域不同:localStorage 持久化本地存储,需要手动删除,sessionStorage 会话级别存储,会话结束,内存随之释放
      2. LocalStorage,sessionStorage 和 cookie 都是遵循同源策略,但 sessionStorage 在于即时相同域名的两个页面,只要他们不在同一个浏览器窗口打开,session 内容也无法共享。
      3. 存储量大,进存在于浏览器端,不与服务端发生通信
        • LocalStorage 用来存储小的稳定资源,base64 图片 不常更新的 css js
        • sessionStorage 用来存储会话级别的信息
      4. indexDB: 运行在浏览器上的非关系型数据库
  • CDN 核心功能
    1. 缓存:就是把资源 copy 到 CDN 服务器上的过程
    2. 回源:就是 CDN 服务器没有这个资源(缓存过期),向根服务器去要这个资源
    3. CDN 和前端性能优化
    • CDN:往往是用来存放静态资源的,根服务器是业务服务器,主要是用在生成动态页面和非纯静态页面,
    • 静态资源:css js 图片等不需要计算得到的资源,
    • 动态资源:后端实时动态生成的资源(jsp asp)

    CDN 服务器存储静态资源,业务服务器存储动态资源,而我们需要的 cookie 默认在同一个域名下是携带 cookie 的。我们把 cdn 和业务服务器分离很完美的避开了 cookie 的出现,解决了不必要 cookie 的资源

渲染层面

  • 服务端渲染机制
  • 服务端解决什么性能问题
  • 服务端渲染应用案例和使用场景

解决: 客户端渲染在页面上呈现的内容,html 中找不到

  • 解决了首屏加载过慢,在客户端渲染模式下,我们除了加载 html 还要等渲染所需要的 js 完成,才能看到页面,会有短暂的白屏现象。服务端渲染模式下,服务器给到客户端已经可以呈现网页,中间环节子啊服务端就帮我们解决到了,所以比较快

    • 服务端渲染把压力分散到了服务器端,要求服务端的服务器比较高。一般使用首屏渲染体验和 SEO 的优化方案(节省成本)
    • 浏览器的兼容性-浏览器的内核决定了浏览器解析网页语法的方式
    • 浏览器内核分为: 渲染进程和 js 引擎
      • 渲染进程包括 HTML 解析器,CSS 解析器 布局 网格 存储 听 音视频 图片解析器等
  • 渲染流程

      1. DOM 树: 解析 HTML 以创建的是 DOM 树(DOM Tree): 渲染引擎开始解析 HTML 文档,转换书中的标签到 DOM 节点,我们成为“内容树”(parserHTML)
      2. CSSOM 树: 解析 CSS 创建出来 CSSOM 树,CSSOM 的解析过程和 DOM 的解析过程是并行的
      3. 渲染树:CSSOM 和 DOM 结合,之后生成渲染树 (render)
      4. 布局渲染树: 从根节点开始递归, 计算出每一个元素的大小,位置等。给么个几点所应该出现的屏幕的精确位置。我们基于渲染树得到布局树(layout)
      5. 绘制渲染树: 遍历渲染树,每个节点将使用 UI 绘制层进行绘制(Paint)
    • css 渲染流程的建议
      • CSS 引擎查找样式表(引擎查找方式: 自右向左)
        1. 避免使用通配符
        2. 关注可以通过继承实现的属性,避免重复匹配定义
        3. 少用选择器标签,用类选择器代替
        4. 减少嵌套(最多不超过三层)
      • CSS 阻塞
      • JS 阻塞(遇到 script 标签,启动 js 引擎,执行完毕后在交给渲染引擎),js 的存在意味着 DOM 的操作,会导致性能的开销, 触发CSS中的回流和重绘

    js和css渲染问题引出为什么js要放在末尾, css放在head头部的问题?

  • DOM渲染优化原理 -

    • 回流:对 DOM 的修改引发 DOM 几何尺寸的变化,浏览器会重新计算,其他元素也都会影响
    • 引发回流的方式
      1. 修改 DOM 结构
      2. 获取 DOM 特定属性值
      3. getComputedStyle(即时计算的值)
    • 解决回流的方案
      1. 利用缓存结局
      2. 避免逐条修改,使用类名合并样式
      3. 使用 display:none 离线化方案
      4. flush 队列
    • 重绘: 对 DOM 样式的修改, 没有影响到几何尺寸的变化,直接绘制新的样式 (背景,颜色, 可见性 visibility:hidden)
    • 重绘不一定发生回流,回流一定会导致重绘, 回流和重绘都会影响性能开销,尽量把回流和重绘次数最小化
    • 减少代码的核心思路就是让 js 给 DOM 分压, JS和DOM在不同的线程。分工协作。
  • 浏览器渲染的机制

    • 宏任务和微任务
      • 宏任务:一个个执行,微任务: 一队列执行
    • DOM 的更新时间点,应该尽可能靠近渲染时机。当我们需要再异步任务中实现 DOM 修改时,把它包装成 micro 任务是一个明智的选择
    • 异步执行的方式: 只看结果,不看过程
      1. 异步队列,批量执行
        • vue 只能怪批量执行的方式: nextTick(源码分析)
      2. 图片懒加载实现
        • lazyload,但是频繁触发会导致大量回到计算,这时候我们需要 throttle(节流)和 debounce(防抖)
        • throttle: 思想在于,某个时间内触发的回调,只认第一个, 一定会触发第一个
          function throttle(fn, wait) {
               let last = 0;
               return function () {
               // 获取调用的上下文
               let context = this;
               let args = arguments;
               let now = new Date(); // 触发的时候
               //   当前时间和上次时间的差值> 等待时间,开始执行第一次
               if (now - last >= wait) {
                   // 第一次触发事件
                   last = now;
                   fn.apply(context, args);
               }
             };
           }
          
        • debounce: 思想在于, 在某段时间内的回调,不管多少次,我只认最后一个,会为每个乘客设置新的等待时间

性能检测工具