web性能优化

97 阅读19分钟

性能优化即用户体验提升,我觉得可以从两个大的方面去入手,一个叫代码优化,一个叫方案优化

代码优化

显而易见就是重新去审视自己的代码,在这个地方不必去纠结太多,主要就是对一些不合理的地方进行代码改进,比如说:

1、合理的循环或递归  
 > 避免在循环体中定义常量,比如某些固定数据或者查询DOM节点
 > 避免循环操作DOM,进而引起页面的回流和重绘
 > 及时的跳出循环
 > 可以适当的考虑使用尾递归,防止栈溢出
2、删除冗余的代码,包括失效的变量、方法、CSS样式等
3、删除多余的脚本文件,CSS样式文件,图片等
4、避免重复的从服务器请求数据,可以合理的使用惰性函数、函数柯里化等高阶函数或者闭包、缓存等方式
5、优先采用并行请求而不是串行请求,例如使用Promise.all或者async/await,防止页面阻塞
6、及时关闭定时器,销毁绑定的事件
7、尽量采用事件委托的方式进行事件绑定,避免大量绑定导致内存占用过多
8、避免嵌套过深的HTML
9、合理的使用CSS选择器,应尽量避免直接使用标签选择器
10、css层级尽量扁平化,避免过多的层级嵌套
11、图片在加载前提前指定宽高或者脱离文档流,可避免加载后的重新计算导致的页面回流;
12、正确使用v-if和v-show
13、合理的给定组件key值
14、优先采用computed
15、利用keep-alive缓存组件
16、只读组件可以使用函数式组件,因为他没有生命周期
17、合理的使用异步组件
18、对于不需要响应式的大数据运用Object.freeze

方案优化

要制定一系列方案来做性能优化,我们就得了解“从输入URL到页面加载完成”这个过程到底发生了什么,然后针对针对这个过程中的方方面面来做优化。一般来说,大致流程包括:

1、通过DNS(域名解析系统)将URL解析为对应的IP地址(DNS解析)
2、与IP地址确定的那台服务器建立TCP网络连接(建立TCP连接)
3、向服务端发送HTTP请求(发送HTTP请求)
4、服务端处理完请求之后,将目标数据放在HTTP响应里返回给客户端(服务端处理请求并返回给客户端)
5、浏览器拿到响应数据,解析响应内容,呈现给用户(浏览器渲染)

从前端层面来看,我们对于DNS解析、TCP连接和服务端处理能做的东西实在有限,所以我们需要在HTTP请求和浏览器渲染上面花费功夫

HTTP请求,我们可以从三个方面入手:减少HTTP请求的次数,缩短HTTP请求的响应时间,缩小HTTP请求的文件体积资源的压缩和合并

1、HTTP压缩:是指在Web服务器和浏览器间传输压缩文本内容的方法
   Wb服务器处理HTTP压缩的工作原理如下:
   (1)Web服务器接收到浏览器的HTP请求后,检查浏览器是否支持HTP压缩;
        在用户浏览器发送请求的HTTP头中,带有" Accept-Encoding:gzip,deflate"参数则表明支持gzip和 deflate两种压缩算法
   (2)如果浏览器支持HTTP压缩,Wb服务器检查请求文件的后缀名;
        -- 如果请求文件是HTML、CSS等静态文件并且文件后缀启用了压缩,
           则Web服务器到压缩缓冲目录中检查是否已经存在请求文件的最新压缩文件
           -- 如果请求文件的压缩文件不存在,Web服务器向浏览器返回未压缩的请求文件,并在压缩缓冲目录中存放请求文件的压缩文件;
           -- 如果请求文件的最新压缩文件已经存在,则直接返回请求文件的压缩文件;
        -- 如果请求文件是ASPX、ASP等动态文件并且文件后缀启用了压缩,
           则Web服务器动态压缩内容井返回浏览器,压缩内容不存到压缩缓存目录中
   HTTP压缩最常用的压缩格式是gzip,所以在服务端开启gzip压缩之后会极大的缩小HTTP请求的文件体积
     // 常见的都是单个值
     Content-Encoding: gzip   // 最常用
     Content-Encoding: deflate  
     Content-Encoding: identity // 表示未经过压缩和修改
     Content-Encoding: br  // brotli

     // Multiple, in the order in which they were applied
     Content-Encoding: gzip, identity
     Content-Encoding: deflate, gzip
     
2、图片压缩,选择正确的图片压缩格式能够很好的提升页面的性能
  jpg:有损压缩,适用于色彩丰富的大图,例如背景图、轮播图、Banner图等
  png:无损压缩,适用于线条感较强、颜色对比强烈的图片,例如稍微复杂点的logo,二维码等
  CSS Sprites:将一些复杂的小图标进行合并,从而减少http请求次数
  svg:文件体积更小,可压缩性更强,适用于矢量图形
  webP:一种更加性能的图片压缩格式,能够有效的缩小http请求的文件体积,但是需要做兼容性处理
  iconfont:适用于线条类型的小图标
  base64编码:适用于体积小,更新频率低的小图标,因为base64编码能够被浏览器直接识别,进而减少了http请求次数,
             我们可以结合webpack的url-loader来做
             
3、按需加载
  图片懒加载,当图片处于可视区域时才去加载
  组件的按需加载,不必在初始的时候加载完全部的组件,只需在调用时加载即可
  路由懒加载,不必在初始的时候加载所有的路由,只需在调用时加载即可
  
4、浏览器缓存
   浏览器缓存按照缓存位置分类,执行顺序如下:
     1、service worker
       (1)Service Worker 能够操作的缓存是有别于浏览器内部的 memory cache 或者 disk cache 的
       (2)memory cache 或者 disk cache 缓存策略以及缓存/读取/失效的动作都是由浏览器内部判断 & 进行的,
            我们只能设置响应头的某些字段来告诉浏览器,而不能自己操作
     2、memory cache
       (1)几乎所有的网络请求资源都会被浏览器自动加入到 memory cache 中;
       (2)但是也正因为数量很大但是浏览器占用的内存不能无限扩大这样两个因素,memory cache 注定只能是个“短期存储”;
       (3)常规情况下,浏览器的 TAB 关闭后该次浏览的 memory cache 便告失效 (为了给其他 TAB 腾出位置);
       (4)而如果极端情况下 (例如一个页面的缓存就占用了超级多的内存),那可能在 TAB 没关闭之前,排在前面的缓存就已经失效了;
       (5)memory cache 机制保证了一个页面中如果有两个相同的请求;
            例如两个 src 相同的 <img>,两个 href 相同的 <link> 都实际只会被请求最多一次,避免浪费
       (6)在从 memory cache 获取缓存内容时,浏览器会忽视例如 max-age=0, no-cache 等头部配置。
            例如页面上存在几个相同 src 的图片,即便它们可能被设置为不缓存,但依然会从 memory cache 中读取
     3、disk cache
       (1)disk cache 会严格根据 HTTP 头信息中的各类字段来判定哪些资源可以缓存,哪些资源不可以缓存;
            哪些资源是仍然可用的,哪些资源是过时需要重新请求的
       (2)当命中缓存之后,浏览器会从硬盘中读取资源,虽然比起从内存中读取慢了一些,但比起网络请求还是快了不少的。
            绝大部分的缓存都来自 disk cache。
            
   如果从上述三类缓存中都没有找到缓存,那么就去服务器上请求数据,然后根据具体场景来决定使用何种缓存方式,具体如下:
     1、根据 Service Worker 中的 handler 决定是否存入 Cache Storage (额外的缓存位置)
     2、根据 HTTP 头部的相关字段(expires, Cache-control / Pragma, last-modified, etag 等)决定是否存入 disk cache
     3、在 memory cache 保存一份资源的引用,以备下次使用

   按失效策略分类
     memory cache 是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受 HTTP 协议头的约束;
     Service Worker 是由开发者编写的额外的脚本,且缓存位置独立,出现也较晚,使用还不算太广泛;
     所以我们平时最为熟悉的其实是 disk cache,也叫 HTTP cache (HTTP缓存);
     平时所说的强缓存,协商缓存,都属于disk cache。

      1、强缓存
        (1)强缓存的含义是,当客户端请求后,会先访问缓存数据库看缓存是否存在。
             如果存在则直接返回;
             不存在则请求真的服务器,响应后再写入缓存数据库
        (2)命中强缓存的情况下,返回的 HTTP 状态码为 2003)强制缓存直接减少请求数,是提升最大的缓存策略
        (4)在考虑使用缓存来优化网页性能的话,强缓存应该是首先被考虑的
        
         可以造成强制缓存的字段是 Cache-control 和 Expires
           Cache-control 的优先级高于 Expires,为了兼容 HTTP/1.0 和 HTTP/1.1,实际项目中两个字段我们都会设置。
          (1)Expires
               这是 HTTP 1.0 的字段,表示缓存到期时间,是一个绝对的时间 (当前时间+缓存时间),
                 如Expires: Thu, 10 Nov 2017 08:45:11 GMT
               设置这个字段之后,就可以告诉浏览器,在未过期之前不需要再次请求。
               但是,这个字段设置时有两个缺点:
                 -- 由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。
                    此外,即使不考虑自信修改,时差或者误差等因素也可能造成客户端与服务端的时间不一致,致使缓存失效。
                 -- 写法太复杂了。表示时间的字符串多个空格,少个字母,都会导致非法属性从而设置失效。2)Cache-control
              (1)已知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,
                该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求
              (2)这两者的区别就是前者是绝对时间,而后者是相对时间。如:Cache-control: max-age=2592000
                   Cache-control 字段常用的值:(完整的列表可以查看 MDN)
                     -- max-age:即最大有效时间,在上面的例子中我们可以看到
                     -- must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。
                     -- no-cache:需要使用协商缓存来验证缓存数据对比来决定。
                     -- no-store: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。
                     -- private:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。
                     -- public:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)
         
      2、协商缓存
      (1)协商缓存的含义是,浏览器先请求缓存数据库,返回一个缓存标识。之后浏览器拿这个标识和服务器通讯。
           如果缓存未失效,则返回 HTTP 状态码 304 表示继续使用,于是客户端继续使用缓存;
           如果失效,则返回新的数据和缓存规则,浏览器响应数据后,再把规则写入到缓存数据库,此时的 HTTP 状态码 200。
      (2)协商缓存在请求数上和没有缓存是一致的,但如果是 304 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,
           因此在响应体体积上的节省是它的优化点。
      (3)对比缓存是可以和强制缓存一起使用的,作为在强制缓存失效后的一种后备方案。实际项目中他们也的确经常一同出现。

       协商缓存有 2 组字段(不是两个):
         (1Last-Modified & If-Modified-Since
               -- 服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间;
                  例如Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
               -- 浏览器将这个值和内容一起记录在缓存数据库中;
               -- 下一次请求相同资源时,浏览器从自己的缓存中找出“不确定是否过期的”缓存,
                   因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段;
               -- 服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比,
                   如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。   
                   
               但是他还是有一定缺陷的:
                 (1)如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。
                 (2)如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到
                      缓存的作用。

        (2)Etag & If-None-Match
             为了解决上述问题,出现了一组新的字段 Etag 和 If-None-Match
             Etag 存储的是文件的特殊标识(一般都是 hash 生成的),服务器存储着文件的 Etag 字段。
             之后的流程和 Last-Modified 一致,
             只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash,
             把 If-Modified-Since 变成了 If-None-Match。服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。

         (3)Etag 的优先级高于 Last-Modified
    
5、本地存储:
    cookie:Cookie就是一个存储在浏览器里的一个小小的文本文件,它附着在 HTTP 请求上,在浏览器和服务器之间“飞来飞去”。
            它可以携带用户信息,当服务器检查 Cookie 的时候,便可以获取到客户端的状态。
            Cookie 是有体积上限的,它最大只能有 4KB
            过量的 Cookie 会带来巨大的性能浪费,因为同一个域名下的所有请求,都会携带 Cookie。
    web storage:localStorage和sessionStorage
      存储容量大: Web Storage 根据浏览器的不同,存储容量可以达到 5-10M 之间。
      仅位于浏览器端,不与服务端发生通信。
      Local Storage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除
      Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放
      Local Storage、Session Storage 和 Cookie 都遵循同源策略。
      Session Storage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 
          Session Storage 内容便无法共享。
    indexedDB:是一个运行在浏览器上的非关系型数据库。
    
6、CDN
  CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。
  这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 
  CDN 提供快速服务,较少受高流量影响。
  CDN 的核心点有两个,一个是缓存,一个是回源。
  “缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,
  “回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的程。
  
7、合理利用打包工具webpack,配置webpack的loader和插件
  1、在配置babel-loader的时候就可以通过配置exclude和cacheDirectory来帮我们避免不必要的转译和缓存转译结果,
  比如对node_modules的转译
  2、使用tree-shaking帮我删除冗余代码
  3、DllPlugin 和 DllReferencePlugin的需要配合使用

浏览器渲染

想要在浏览器渲染上做文章,那么就要先了解一下浏览器的渲染过程,大致上包括:

1、解析html,构建dom树;解析css生成css规则树
   浏览器解析 HTML 文档的源码,然后构造出一个 DOM 树,DOM 树的构建过程是一个深度遍历的过程,当前节点的所有子节点都构建好
   以后才会去构建当前节点的下一个兄弟节点。
   浏览器对 CSS 文件内容进行解析,一般来说,浏览器会先查找内联样式,然后是 CSS 文件中定义的样式,最后再是浏览器默认的样
   式,构建 CSS Rule Tree。
2、构建render树
  根据dom树和css规则树构造rendering tree
3、布局render树
   有了rendering tree,浏览器已经能知道网页中有哪些节点、各个节点的css定义以及他们的从属关系,从而去计算出每个节点在屏幕
   中的位置。
4、绘制render树
5、回流
  当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染,内行称这个回退的过程叫reflow
6、重绘
  改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变

了解了渲染过程那么就可以针对这个过程采取一些优化措施,渲染层面的优化其实大部分可以在做代码优化时就考虑, 这里主要提一下节流和防抖

节流和防抖
  (1)这两个东西都以闭包的形式存在。
  (2)它们通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 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);
       }
     }
   }

   Debounce
   防抖的中心思想在于:设置一个时间,在这个时间内,不管你触发了多少次回调,我都只认最后一次,在最后一次重新开始计时,并在
                       计时结束时给予响应,前面的回调都会被重置。

   // fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
   function debounce(fn, delay) {
     // 定时器
     let timer = null

     // 将debounce处理结果当作函数返回
     return function () {
       // 保留调用时的this上下文
       let context = this
       // 保留调用时传入的参数
       let args = arguments

       // 每次事件被触发时,都去清除之前的旧定时器
       timer && clearTimeout(timer)
       
       // 设立新定时器
       timer = setTimeout(function () {
         fn.apply(context, args)
       }, delay)
     }
  }
  
  用 Throttle 来优化 Debounce
  debounce 的问题在于它“太有耐心了”。试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下
  一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,
  用户同样会产生“这个页面卡死了”的观感。

  为了避免弄巧成拙,我们需要借力 throttle 的思想,打造一个“有底线”的 debounce——等你可以,但我有我的原则:delay 时间内,
  我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。这个 throttle 与 debounce “合体”思路,已经被
  很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中:

  // 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)
      }
    }
  }

感谢 @小蘑菇哥哥 / @北海北方 / @修言 提供的优质文章

一文读懂前端缓存

HTTP缓存机制

前端性能优化原理与实践