移动端性能优化

4,153 阅读20分钟

移动端性能优化

初识移动端性能优化

什么是性能

  • 页面的响应速度

页面的响应速度

  • 打开页面到实际能够正常使用的时间

    • 网络请求的时间
    • 页面加载和渲染的时间
  • 与页面进行交互的流畅程度

    • Javascript脚本的执行速度

更快地加载页面首屏内容,无需考虑整张页面

为什么需要优化移动端的性能

  • 提升用户体验
  • 与PC端相比,移动端网络速度慢
  • 与PC端相比,移动端设备性能低

学什么

  • 各阶段的性能优化点
  • 具体的性能优化策略

网络请求过程中的优化点

image-20211204173318006

image-20211204173331306

Chrome Dev Tool 中时间线各阶段代表的意义

image-20211204173345511

在请求能够被发出去前的等等时间。包含了用于处理代理的时间。

另外,如果有已经建立好的连接,那么这个时间还包括等待已建立连接被复用的时间,这个遵循Chrome对同一源最大6个TCP连接的规则。

Proxy Negotiation

  • 与代理服务器连接的协商的时间。

DNS Lookup

  • 用于执行DNS查找时间。页面上的每一个新域需要一个完整的来回做DNS查找。

Initial Connection / Connecting

  • 用于建立链接的时间,包括TCP握手及多次尝试握手,还有处理SSL。

SSL

  • 完成SSL握手的时间。

Request Sent / Sending

  • 发起请求的时间

Waiting (TTFB)

  • 请求发出后,到收到响应的第一个字节所花费的时间(Time To First Byte)

Content Download / Downloading

  • 用于下载响应的时间

将多个资源分布在不同域上,减少请求队列的等待时间

  • 浏览器为每个域名分配的并发通道有限
  • 多个域意味着更多的DNS查询时间,通常把域名拆分到3~5个比较合适

通过dns-prefetch减少DNS查询时间

  • 尝试在请求资源之前解析域名

image-20211204173419755

  • 仅对跨域域上的DNS查找有效
  • 已经解析过的域名不要再添加dns-prefetch

image-20211204173408801

减少HTTP请求数量

  • 资源的合并(合并CSS、JS文件)
  • 静态资源缓存
    • 静态资源加hash后缀,根据文件内容计算hash
    • 文件内容不变,则hash不变,则url不变
    • url和文件不变,则会自动触发http缓存机制,返回304
  • 内联首屏相关代码
  • 使用缓存(浏览器缓存、localStorage 等)
  • 使用 CDN 让资源加载更快
<!-- <link rel="stylesheet" href="./css/reset.css" />
<link rel="stylesheet" href="./css/base.css" />
<link rel="stylesheet" href="./css/index.css" /> -->

<!-- <link rel="stylesheet" href="./css/index.css" /> -->
	
<!-- 1.合并后的资源不能过大 -->
<!-- 2.考虑缓存的问题 -->
<link rel="stylesheet" href="./css/common.css" />
<link rel="stylesheet" href="./css/index.css" />
//使用
<script src="https://cdn.bootcss.com/zepto/1.0rc1/zepto.min.js"></script>

image-20211204173430979

减少请求资源的大小

  • 资源的压缩(HTML、 CSS 的压缩以及JS的压缩和混淆)
  • 开启Gzip压缩
  • 减少Cookie体积

image-20211204173446079

image-20211204173455034

image-20211204173523568

使用HTTP2

  • 多路复用:最有价值的优点,解决了线头阻塞的问题,在浏览器可并行发送 N 条请求。
  • 首部压缩:更小的负载体积。
  • 新的二进制格式:http1.x 是文本格式传输,http2是二进制格式传输。
  • 服务端推送:服务器端可以主动向客户端推送资源。

页面加载和渲染过程中的优化点

image-20211204173535819

  • CSS 一般在head中引入
  • JavaScript一般在body末尾引入

image-20211204173545246

  • 减少回流/重布局/重排(Reflow/Relayout) 与重绘(Repaint)
    • 元素的尺寸、位置、隐藏等属性改变时,浏览需要重新计算,就称为回流
    • 元素的外观、风格等属性改变时,浏览器只需要重新绘制,就称为重绘
    • 回流一定会引起重绘,重绘不一定会引起回流

JavaScript脚本中的优化点

  • DOM操作优化
  • 事件优化
  • 图片懒加载和预加载

图片优化

减少HTTP请求数量

  • 使用CSS画图(动画)代替简单的图片(www.webhek.com/post/40-css…)
  • 合并小图标(CSS Sprites)
  • 将小图标内嵌到HTML中(Base64格式的图片)

减少请求资源的大小

  • 使用图标字体代替简单的图标
  • 压缩图片
  • 选择合适的图片大小
  • 选择合适的图片类型

图片类型

  • jpg
    • 有损压缩,压缩率高,不支持透明
    • 适用于色彩丰富、渐变色且不需要透明图片的场景
  • png
    • png-8 256色+支持透明
    • png-24 2^24色+不支持透明
    • png-32 2^24色+支持透明
    • 适用于大部分需要透明图片的场景
  • webp
    • 与png、jpg相比,相同的视觉体验下,图像更小
    • 支持有损压缩、无损压缩、透明和动画
    • 理论上完全可以替代png、jpg、 gif 图片格式
    • 存在一定的兼容性问题

动画优化

image-20211204173559471

  • 优先使用CSS3过渡和动画
  • 优先使用translate3d做运动
  • 必须使用JavaScript做动画时,使用requestAnimationFrame
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>动画优化</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      .mask {
        position: fixed;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.5);
        opacity: 1;
        /* transition: opacity 0.5s; */
      }
    </style>
  </head>
  <body>
    <div id="mask" class="mask"></div>

    <script>
   		 // 使用Js搭配CSS3完成元素消失的动画
        const $mask = document.getElementById('mask');



        $mask.addEventListener(
            'click',
            function () {
                $mask.style.opacity = 0;
            },
            false
        );
        $mask.addEventListener(
            'transitionend',
            function () {
                $mask.style.display = 'none';
            },
            false
        );
    </script>
  </body>
</html>

  • 使用JS requestAnimationFrame 来实现
    • window.requestAnimationFrame() 需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行一次。
$mask.addEventListener(
    "click",
    function () {
        // setTimeout(fadeOut, 20);
        requestAnimationFrame(fadeOut);
    },
    false
);

let opacity = 1;

function fadeOut() {
    opacity -= 0.05;

    if (opacity <= 0) {
        opacity = 0;
        $mask.style.display = "none";
    } else {
        requestAnimationFrame(fadeOut);
    }
    $mask.style.opacity = opacity;
}

CSS优化

选择器优化

  • 不要使用嵌套过多过于复杂的选择器,保持简单,可以通过样式直接选择,不要画蛇添足。
  • 避免过多的通配符选择器
  • 移除无匹配的样式
/* 较差的做法 */
ul li a {
    text-decoration: none;
}
ul.list li.list-item a.list-link {
    text-decoration: none;
} 
/* 更好的做法 */
.list-link {
    text-decoration: none;
}

/* 避免过多的通配符选择器 */
/* 少量完全可以 */
* {
    padding: 0;
    margin: 0;
} 

/* 1.4.移除无匹配的样式 */
.list {
} 

其他优化

  • 提取公用部分
  • 避免使用 CSS @import 导入 CSS
/* 提取公用部分 */
/* 较差的做法 */
ol {
    padding: 0;
    margin: 0;
}
p {
    padding: 0;
    margin: 0;
}
/* 更好的做法 */
ol,
p {
    padding: 0;
    margin: 0;
}

<syule>
/* 避免使用 CSS @import 导入 CSS 会发送多余的 HTTP 请求 (less sass里面的@import可以使用 因为相对于拷贝一份代码到当前页面,不会发送额外请求)*/
 @import "./reset.css";
</style>

简写css颜色属性值

/* 不推荐 */
.box{ color:#000000; background-color:#ddeeff; }
/* 推荐 */
.box{ color:#000; background-color:#def;}

删除css属性值为0的单位

0就是0,任何单位都不需要,只要前面的数值为0,后面的单位都可以去掉 .

/* 不推荐 */
.box{ margin:0px; padding:0px;}
/* 推荐 */
.box{ margin:0; padding:0;}

DOM优化

渲染优化

  • 减少 DOM 元素数量和嵌套层级

  • 尽量避免使用 table 布局,用其他标签代替

  • table 是作为一个整体解析的,要等整个表格都解析完成才显示可能很小的一点改动,也会造成整个 table 的重新布局

    • table {
      	width: 100%;
      	border-collapse: collapse;
      }
      th,
      td {
      	border: 1px solid #ccc;
      	text-align: center;
      }
      
      <table>
          <tr>
              <th>姓名</th>
              <th>年龄</th>
              <th>性别</th>
          </tr>
          <tr>
              <td>张三</td>
              <td>18</td>
              <td></td>
          </tr>
          <tr>
              <td>李四</td>
              <td>20</td>
              <td></td>
          </tr>
      </table>
      

JS选择器优化

  • 优先使用 id 来获取单个元素

    • console.log(document.getElementById('box')); // 推荐
      console.log(document.querySelector('#box')); // 不推荐
      
  • 获取多个元素时,尽量直接通过元素本身的 className 获取

    • console.log(document.querySelectorAll('ul.list li.item')); // 不推荐
      console.log(document.getElementsByClassName('item')); // 推荐
      console.log(document.querySelectorAll('.item')); // 推荐
      

减少DOM操作次数

  • 总是将选择器的选择结果缓存起来

    • // 总是将选择器的选择结果缓存起来
      const $list = document.getElementById('list');
      
  • 避免在循环中多次使用 innerHTML,在循环结束后使用一次

    • const $list = document.getElementById('list');
      const todoDatas = ['洗衣服', '做饭', '写代码'];
      // 错误写法
      for (const item of todoDatas) {
          $list.innerHTML += `<li class="item">${item}</li>`;
      }
      
      // 正确的写法 避免在循环中多次使用 innerHTML,在循环结束后使用一次
      let html = '';
      for (const item of todoDatas) {
          html += `<li class="item">${item}</li>`;
      }
      $list.innerHTML = html;
      
  • 新创建的元素,完成必要操作后再添加到页面中

    • for (const item of todoDatas) {
          const $li = document.createElement("li");
      
          // 新创建的元素,完成必要操作后再添加到页面中
          $li.className = "item";
          $li.innerHTML = item;
          $li.style.color = "pink";
      
          $list.appendChild($li);
      }
      
  • 使用 DocumentFragment 优化多次的 appendChild

    • // 使用 DocumentFragment 优化多次的 appendChild
      const $liFragment = document.createDocumentFragment();
      
      for (const item of todoDatas) {
          const $li = document.createElement('li');
      
          $li.className = 'item';
          $li.innerHTML = item;
          $li.style.color = "pink";
      
          $liFragment.appendChild($li);
      }
      $list.appendChild($liFragment);
      
  • 不要直接通过 JS 修改元素的 style,通过添加移除 class 修改元素样式

    • <style>
          .box {
              width: 100px;
              height: 100px;
              background-color: pink;
          }
          .active {
              width: 200px;
              height: 200px;
              background-color: green;
          }
      </style>
      
       const $box = document.getElementById("box");
       // let active = false;
      $box.addEventListener(
          "click",
          () => {
              if (!active) {
                  active = true;
                  // $box.style.width = '200px';
                  // $box.style.height = '200px';
                  // $box.style.backgroundColor = 'green';
      
                  $box.classList.add("active");
              } else {
                  active = false;
                  // $box.style.width = '100px';
                  // $box.style.height = '100px';
                  // $box.style.backgroundColor = 'pink';
                  $box.classList.remove("active");
              }
      
              $box.classList.toggle("active");
          },
          false
      );
      
  • 注意强制回流

    • 当获取的属性值包括但不限于 offsetTop、offsetLeft、scrollTop、clientTop 这些“全局属性”时,需要此时页面上的其他元素的布局和样式处于最新状态,这会引起多次的回流和重绘。这样的操作称为强制回流
    • image-20211204173627079
    • gist.github.com/paulirish/5…
    • 可以将其结果缓存起来,需要更新的时候再更新
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Document</title>
        <style>
            .backtop {
                position: fixed;
                right: 20px;
                bottom: 20px;
                width: 90px;
                height: 90px;
                line-height: 90px;
                text-align: center;
                background-color: rgba(0, 0, 0, 0.6);
                border-radius: 50%;
                color: #fff;
                font-size: 60px;
                text-decoration: none;
                -webkit-tap-highlight-color: transparent;
            }
            .none {
                display: none;
            }

        </style>
    </head>
    <body style="height: 5000px;">
        <a href="#" id="backtop" class="backtop">&uarr;</a>

        <script>
            // 注意强制回流
            const $backtop = document.getElementById("backtop");
            //做缓存 窗口改变的时候再重新计算
            let winHeight = window.innerHeight;

            window.addEventListener(
                "resize",
                () => {
                    winHeight = window.innerHeight;
                },
                false
            );

            window.addEventListener("scroll", scrollHandler, false);

            function scrollHandler() {
                // console.log('scroll');
                // 不推荐这样 两个属性都会造成强制回流 
                // 可以对window.innerHeight进行处理
					// if(document.documentElement.scrollTop >  window.innerHeight)
                if (document.documentElement.scrollTop >= winHeight) {
                    $backtop.classList.remove("none");
                } else {
                    $backtop.classList.add("none");
                }
            }
        </script>
    </body>
</html>

事件代理

什么是事件代理 事件代理的实现

什么是事件代理

  • 也叫事件委托,把原本在子元素上监听的事件委托给父元素,让父元素监听 利用事件冒泡的机制

事件代理的实现

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>事件代理</title>
        <style>
            * {
                box-sizing: border-box;
            }
            body {
              background-color: #f5f5f5;
          }
          .input {
              width: 100%;
              height: 40px;
              border: 1px solid #ccc;
              margin-bottom: 20px;
              font-size: 20px;
          }
          .list {
              padding: 0;
              margin: 0;
            }
            .item {
                display: flex;
                justify-content: space-between;
                padding: 0 10px;
                margin-bottom: 10px;

                background-color: #fff;

                font-size: 40px;
            }

            .del {
                text-decoration: none;
            }
        </style>
    </head>
    <body>
        <input type="text" id="input" class="input" placeholder="请输入待办事项" />
        <ul class="list" id="list">
            <li class="item">洗衣服<a href="javascript:;" class="del">x</a></li>
            <li class="item">做饭<a href="javascript:;" class="del">x</a></li>
            <li class="item">写代码<a href="javascript:;" class="del">x</a></li>
        </ul>

        <script>


            const $input = document.getElementById('input');
            const $list = document.getElementById('list');

            // 使用事件委托到父元素 性能消耗小 原理:冒泡
            $list.addEventListener(
                'click',
                evt => {
                    // console.log('click');
                    // console.log(evt.target);
                    if (evt.target.classList.contains('del')) {
                        $list.removeChild(evt.target.parentNode);
                    }
                },
                false
            );

            $input.addEventListener(
                'keypress',
                evt => {
                    // console.log(evt);
                    if (evt.keyCode === 13) {
                        // 回车
                        if (!$input.value) return;

                        const $item = document.createElement('li');
                        const $del = document.createElement('a');
                        $item.className = 'item';
                        $del.className = 'del';
                        $del.href = 'javascript:;';

                        $item.innerHTML = $input.value;
                        $del.innerHTML = 'x';
								
                        
                        // 每次创建都会给a标签绑定事件函数 消耗性能
                        // $del.addEventListener(
                        //   'click',
                        //   () => {
                        //     $list.removeChild($item);
                        //   },
                        //   false
                        // );

                        $item.appendChild($del);
                        $list.appendChild($item);

                        $input.value = '';
                    }
                },
                false
            );
        </script>
    </body>
</html>

事件稀释

什么是事件稀释

  • 有些事件在一段时间内会多次触发,事件稀释就是减少这些事件的触发频率

事件稀释的方法

  • 防抖

    • 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时(施法时间)
  • 节流

    • 每隔一段时间,只执行一次函数(冷却时间)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>事件稀释</title>
    <style>
      .backtop {
        position: fixed;
        right: 20px;
        bottom: 20px;
        width: 90px;
        height: 90px;
        line-height: 90px;
        text-align: center;
        background-color: rgba(0, 0, 0, 0.6);
        border-radius: 50%;
        color: #fff;
        font-size: 60px;
        text-decoration: none;
        -webkit-tap-highlight-color: transparent;
      }
      .none {
        display: none;
      }
    </style>
  </head>
  <body style="height: 2000px">
    <a href="#" id="backtop" class="backtop none">&uarr;</a>
		
    <script>
      // 什么事件需要稀释
      // 比如 scroll resize mousemove touchmove 等
      // window.addEventListener('scroll', handler, false);
      // window.addEventListener('resize', handler, false);
      // window.addEventListener('mousemove', handler, false);
      // window.addEventListener('touchmove', handler, false);
      // function handler(evt) {
      //   console.log(evt.type);
      // }

      // window.addEventListener('scroll', debounce(scrollHandler), false);
      window.addEventListener("scroll", throttle(scrollHandler), false);

      const $backtop = document.getElementById("backtop");
      let winHeight = window.innerHeight;

      window.addEventListener(
        "resize",
        debounce(() => {
          winHeight = window.innerHeight;
          console.log(winHeight);
        }),
        false
      );

      function scrollHandler() {
        console.log("scroll");

        if (document.documentElement.scrollTop >= winHeight) {
          $backtop.classList.remove("none");
        } else {
          $backtop.classList.add("none");
        }
      }

      // 防抖 debounce
      function debounce(fn, miliseconds = 250, context) {
        let timer = null;
		  // debounce 调用后生成的函数 a(名字随意)
        return function (...args) {
          const self = context || this;

          if (timer) {
            clearTimeout(timer);
          }

          timer = setTimeout(() => {
            fn.apply(self, args);
            timer = null;
          }, miliseconds);
        };
      }

      // 节流 throttle
      function throttle(fn, miliseconds = 250, context) {
        let lastEventTimestamp = null;
			
        return function (...args) {
           // 指定this 要么是传入的 否则就是当前返回函数的this
          const self = context || this;
           // 记录当前时间戳
          const now = Date.now();
			 // 如果时间戳为false值 或 当前时间 - 之前时间 >= 规定时间 则进入语句执行代码
          if (!lastEventTimestamp || now - lastEventTimestamp >= miliseconds) {
             // 更新之前时间的值
            lastEventTimestamp = now;
             // 执行传入函数 绑定this 传入args
            fn.apply(self, args);
          }
        };
      }
    </script>
  </body>
</html>
//模拟一段ajax请求
function ajax(content) {
    console.log('ajax request ' + content)
}

let inputc = document.getElementById('throttle')

let throttleAjax = throttle(ajax, 1000)
// let debounceAjax = debounce(ajax, 500)

inputc.addEventListener('keyup', e => {
    throttleAjax(e.target.value)
    //debounceAjax(e.target.value)
})

回顾一下之前的返回顶部显示与隐藏实现

image-20211204173715171

scrollHandler 函数没有参数,也没有用到 this,所以我们调用 debounce 防抖函数的时候,就只传递了一个参数(就是 scrollHandler 函数)。

现在我们改写一下 scrollHandler 函数,用到参数和 this

image-20211204173725995

我们把原来的显示和隐藏的临界值换成了参数 threshold,把 $backtop 换成了 this。

所以我们希望调用 debounce 防抖函数的时候,能向 scrollHandler 中传入参数 threshold,并且制定 this 的值为 $backtop。

image-20211204173745431

这里我们调用 debounce 函数的时候传入了第三个参数(context),指定了 scrollHandler 中的this为 $backtop。

debounce 函数调用后返回一个函数 a(名字讲解,随便起的,不重要),a函数调用了bind() 方法,调用后也返回一个函数,而 addEventListener 的第二个参数就需要函数,所以这里调用 bind 方法没问题。

bind() 方法的第一个参数,可以指定函数 a 的 this 指向,由于我们之前传递了 context($backtop),所以这里传 null就可以。

bind() 方法从第二个参数开始,可以向函数 a 中传递参数,由 args 接收。这里我们把 winHeight(当前视口的高度)作为临界值传递进去。

image-20211204173801729

至此,我们就演示了 args 和 context 的用法。节流函数和防抖函数类似,不再赘述。

图片懒加载

什么是图片懒加载

  • 图片懒加载又叫图片延迟(按需)加载
  • 在需要的时候加载图片
  • 更好的加载页面的首屏内容 无需考虑整个页面

图片懒加载的实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>图片懒加载</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }

      img {
        width: 100%;
        height: 183px;
      }
    </style>
  </head>
  <body>
    <div class="img-container">
      <img
        src="https://gitee.com/z1725163126/cloundImg/raw/master/loading.gif"
        alt=""
        data-src="https://gitee.com/z1725163126/cloundImg/raw/master/1.jpg"
        class="lazyload"
      />
    </div>
    <div class="img-container">
      <img
        src="https://gitee.com/z1725163126/cloundImg/raw/master/loading.gif"
        alt=""
        data-src="https://gitee.com/z1725163126/cloundImg/raw/master/2.jpg"
        class="lazyload"
      />
    </div>
    <div class="img-container">
      <img
        src="https://gitee.com/z1725163126/cloundImg/raw/master/loading.gif"
        alt=""
        data-src="https://gitee.com/z1725163126/cloundImg/raw/master/3.jpg"
        class="lazyload"
      />
    </div>
    <div class="img-container">
      <img
        src="https://gitee.com/z1725163126/cloundImg/raw/master/loading.gif"
        alt=""
        data-src="https://gitee.com/z1725163126/cloundImg/raw/master/4.jpg"
        class="lazyload"
      />
    </div>
    <div class="img-container">
      <img
        src="https://gitee.com/z1725163126/cloundImg/raw/master/loading.gif"
        alt=""
        data-src="https://gitee.com/z1725163126/cloundImg/raw/master/5.jpg"
        class="lazyload"
      />
    </div>

    <script>
      // 图片要设置高度
      const imgs = [...document.querySelectorAll(".lazyload")];

      lazyload();

      window.addEventListener("scroll", lazyload, false);

      function lazyload() {
        for (let i = 0; i < imgs.length; i++) {
          const $img = imgs[i];

          if (isInVisibleArea($img)) {
            $img.src = $img.dataset.src;
            imgs.splice(i, 1);
            i--;
          }
        }
      }

      // DOM 元素是否在可视区域内
      function isInVisibleArea($el) {
        const rect = $el.getBoundingClientRect();
        // console.log(rect);

        return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
      }
    </script>
  </body>
</html>

  • 加上防抖和节流进行性能优化试试

 window.addEventListener("scroll", debounce(lazyload), false);

// 不适用
// window.addEventListener('scroll', throttle(lazyload), false);

// 防抖 debounce
// 在某个时间期限内,事件处理函数只执行一次
function debounce(fn, miliseconds = 250, context) {
    let timer = null;

    return function (...args) {
        const self = context || this;

        if (timer) {
            clearTimeout(timer);
        }

        timer = setTimeout(() => {
            fn.apply(self, args);
            timer = null;
        }, miliseconds);
    };
}

// 节流 throttle
// 事件处理函数执行一次后,在某个时间期限内不再工作
function throttle(fn, miliseconds = 250, context) {
    let lastEventTimestamp = null;

    return function (...args) {
        const self = context || this;
        const now = Date.now();

        if (!lastEventTimestamp || now - lastEventTimestamp >= miliseconds) {
            lastEventTimestamp = now;
            fn.apply(self, args);
        }
    };
}

图片预加载

什么是图片预加载

  • 提前加载将来可能会用到的图片

图片预加载的实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>图片预加载</title>
    <style>
      .img-container {
        display: flex;
        align-items: center;
        height: 100vh;
        background-color: rgba(0, 0, 0, 0.5);
      }
      img {
        width: 100%;
      }
      * {
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <div class="img-container">
      <img src="https://gitee.com/z1725163126/cloundImg/raw/master/1.jpg" alt="图片" id="img" />
    </div>

    <script>
      const imgs = [
        "https://gitee.com/z1725163126/cloundImg/raw/master/2.jpg",
        "https://gitee.com/z1725163126/cloundImg/raw/master/3.jpg",
        "https://gitee.com/z1725163126/cloundImg/raw/master/4.jpg",
        "https://gitee.com/z1725163126/cloundImg/raw/master/5.jpg",
      ];
      let i = 0;
      const $img = document.getElementById("img");

      // 页面一开始调用preload加载数组的第一个元素
      preload(imgs[i])
        .then((data) => {})
        .catch(() => {});

      // 点击切换
      $img.addEventListener(
        "click",
        () => {
          // 当索引小于数组length
          if (i < imgs.length) {
            // 将数组元素的src赋值给页面元素
            $img.src = imgs[i];
            // i+1 下次点击变为数组的第二个元素 依次递增
            i++;
            // 当索引小于数组length
            if (i < imgs.length) {
              // 预加载下一个图片
              preload(imgs[i]);
            }
          } else {
            // 当 索引和 数组length相同 则数组内没元素了
            console.log("已经是最后一张了!");
          }
        },
        false
      );

      // 预加载
      function preload(src) {
        // Promise进行包装
        return new Promise((resolve, reject) => {
          // 创建一个新的图片标签
          const image = new Image();

          // 图片加载完成调用成功状态
          image.addEventListener("load", () => resolve(image), false);
          // 图片加载失败调用失败状态
          image.addEventListener("error", () => reject(), false);
          // 将传进来的src赋值给新的图片
          image.src = src;
        });
      }
    </script>
  </body>
</html>


扩展:为什么第二次打开页面快

  • 第一次加载页面过程中,缓存了一些数据,之后再加载就直接从缓存中获取而不用请求服务器,所以速度更快,也减轻了服务器的压力

  • 网络方面的缓存分为三块:DNS缓存HTTP缓存(浏览器缓存)CDN缓存

  • 本地存储离线存储,更能提高首屏加载速度

DNS缓存

  • 进入页面的时候会进行DNS查询,找到域名对应的服务器的IP地址,再发送请求
  • DNS域名查找先在客户端进行递归查询

image-20211204173818050

  • 在任何一步找到就会结束查找流程,而整个过程客户端只发出一次查询请求
  • 如果都没有找到,就会走DNS服务器设置的转发器,如果没设置转发模式,则向13根发起解析请求,这里就是迭代查询,如图

13根: 全球共有13个根域服务器IP地址,不是13台服务器! 因为借助任播技术,可以在全球设立这些IP的镜像站点,所以访问的不是唯一的那台主机

image-20211204173834791

  • 很明显,整个过程会发出多次查询请求

  • 在第一次进入页面后就会把DNS解析的地址记录缓存在客户端,之后再进的话至少不需要发起后面的迭代查询了,从而速度更快

HTTP缓存

  • 就是将http请求获取的页面资源存储在本地,之后再加载直接从缓存中获取而不用请求服务器,从而响应更快。先看图:

image-20211204173857682

强缓存

第一次请求时,服务器把资源的过期时间通过响应头中的ExpiresCache-Control两个字段告诉浏览器之后再请此求这个资源的话,会判断有没有过期,没有过期就直接拿来用,不向服务器发起请求,这就是强缓存

Expires

  • 用来指定资源到期绝对时间,服务器响应时,添加在响应头中。
expires: Wed, 22 Nov 2021 08:41:00 GMT

ps:如果服务器和浏览器端时间不一致的话可能导致失败。比如现在时间是8月1,expires过期时间是8月2,客户端把电脑时间改成了8月3,那就用不了这个缓存

Cache-Control

  • 指定资源过期时间秒,如下,表示在这个请求正确返回后的300秒内,资源可以使用,否则过期
cache-control:max-age=300

为什么指定缓存过期时间需要两个字段呢?

  • 因为有的浏览器只认识 Cache-Control,有的浏览器不认识,不认识的情况下再找 Expires

Expires 和 Cache-Control 的区别

  • Expires 是HTTP/1.0中的,Cache-Control 是HTTP/1.1中的;
  • Expires 是为了兼容,在不支持 HTTP/1.1 的情况下才会发生作用
  • 两者同时存在的话 Cache-Control 优先级高于 Expires;

Cache-Control请求头常见属性

image-20211204173909593

ps:多少秒是自定义的,我这里写死是方便理解

Cache-Control响应头常见属性

image-20211204173917582

强缓存的缺点

  • 就是缓存过期之后,不管资源有没有变化,都会重新发起请求,重新获取资源
  • 而我们希望的是在资源文件没有更新的情况下,即使过期了也不重新获取资源,继续使用旧资源
  • 所以协商缓存它来了,在强缓存过期的情况下,再走协商缓存的流程,判断文件有没有更新
协商缓存
  • 第一次请求资源时,服务器除了会返回给浏览器上面说的过期时间,还会在响应头添加 Last-Modified 字段,告诉浏览器该资源的最后修改时间
last-modified: Fri, 27 Oct 2021 08:35:57 GMT
  • 然后浏览器再次请求的时候就把这个时间再通过另一个字段If-Modified-Since,发送给服务器
if-modified-since: Fri, 27 Oct 2021 08:35:57 GMT
  • 服务器再把这两个字段的时间对比,如果是一样的,就说明文件没有被更新过,就返回状态码304和空响应体给浏览器,浏览器直接拿过期了的资源继续使用即可
  • 如果对比不一样说明资源有更新,就返回状态码200和新的资源

image-20211204173942838

所以说Last-Modified/If-Modified-Since它俩是成对的,是为了对比文件修改时间

缺点

  • 如果本地打开了缓存文件,即使没有对文件进行修改,但还是会造成Last-Modified被修改,服务器端不能命中缓存导致发送相同资源
  • 因为Last-Modified只能以秒计时,如果在不可感知的时间内修改了文件,服务器端会认为还是命中了,无法返回正确的资源
  • 如果资源有周期性变化,如资源修改后,在一个周期内又改回了原来的样子,我们认为这个周期前的缓存是可以使用的,但是Last-Modified不这样认为

因为这些缺点,所以便有了另外一对 ETag/If-None-Match,用来对比文件内容

ETag/If-None-Match

第一次请求资源时,服务器除了会在响应头上返回ExpiresCache-ControlLast-Modified,还在返回Etag字段,表示当前资源文件的一个唯一标识。

这个标识符由服务器基于文件内容编码生成,能精准感知文件的变化,只要文件内容不同,ETag就会重新生成

etag: W/"132489-1627839023000"
  • 然后浏览器再次请求的时候就把这个文件标识 再通过另一个字段 If-None-Match,发送给服务器
if-none-match: W/"132489-1627839023000"
  • 服务器再把这两个字段的时间对比,如果发现是一样的,就说明文件没有被更新过,就返回状态码304和空响应体给浏览器,浏览器直接拿过期了的资源继续使用
  • 如果对比不一样说明资源有更新,就返回状态码200和新的资源

Last-Modified 和 ETag 的区别

  • Etag 感知文件精准度要高于 Last-Modified
  • 同时使用时,服务器校验优先级 Etag/If-None-Match
  • Last-Modified 性能上要优于 Etag,因为 Etag 生成过程中需要服务器付出额外开销,会影响服务器端的性能,所以它并不能完全替代 Last-Modified,只能作为补充和强化
强缓存与协商缓存的区别
  • 优先查找强缓存,没有命中再查找协商缓存
  • 强缓存不发请求到服务器,所以有时候资源更新了浏览器还不知道,但是协商缓存会发请求到服务器,资源是否有更新,服务器肯定知道
  • 目前项目大多数使用缓存文案
    • 协商缓存一般存储:HTML
    • 强缓存一般存储:css, image, js,文件名带上 hash

启发式缓存

就是响应中没有ExpiresCache-Control:max-ageCache-Control:s-maxage,并且设置了Last-Modified时

浏览器默认会采用一个启发式的算法,即启发式缓存,来计算缓存有效期

通常会根据响应头中的Date字段(报文创建时间)减去Last-Modified值的10%作为缓存时间

max(0,(Date - Last-Modified)) % 10

缓存实际使用策略

对于频繁变动的资源
  • 使用Cache-Control:no-cache,使浏览器每次都请求数据,然后配合EtagLast-Modified来验证资源是否有效,这样虽然不能节省请求数量,但能显著减少响应数据大小
对于不常变化的资源
  • 可以给它们的Cache-Control配置一个很大的max-age=31536000(一年),这样浏览器之后请求相同的URL会命中强缓存,而为了解决更新问题,就需要在文件名(或者路径)中添加hash,版本号等动态字符,之后更改动态字符,从而达到更改引用URL的目的,让之前的强缓存失效(其实并未立即失效,只是不再使用了而已)
缓存存放位置,和读取的优先级

优先级就是按下面顺序

1. Service Worker

2. Memory Cache(内存)

就是将资源存储在内存中,下次访问直接从内存中读取。例如刷新页面时,很多数据都是来自于内存缓存。一般存储脚本、字体、图片。

优点是读取速度快;缺点由于一旦关闭Tab标签页,内存中的缓存也就释放了,所以容量和存储时效上差些

3. Disk Cache(硬盘)

就是将资源存储在硬盘中,下次访问时直接从硬盘中读取。它会根据请求头中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使是跨域站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次请求。

优点是缓存在硬盘中,容量大,并且存储时效性更长;缺点是读取速度慢些

4. Push Cache

这个是推送缓存,是HTTP/2中的内容,当上面三种缓存都没有命中时才会,被使用。它只会存在于Session中,一旦会话结束就会释放,所以缓存时间很短,而且Push Cache中的缓存只能被使用一次

CDN缓存

  • 当我们发送一个请求时,浏览器本地缓存失效的情况下,CDN会帮我们去计算哪得到这些内容的路径短而且快。
  • 比如在广州请求广州的服务器就比请求新疆的服务器响应速度快得多,然后向最近的CDN节点请求数据
  • CDN会判断缓存数据是否过期,如果没有过期,则直接将缓存数据返回给客户端,从而加快了响应速度。如果CDN判断缓存过期,就会向服务器发出回源请求,从服务器拉取最新数据,更新本地缓存,并将最新数据返回给客户端。
  • CDN不仅解决了跨运营商和跨地域访问的问题,大大降低访问延时的同时,还起到了分流的作用,减轻了源服务器的负载

几种刷新和回车的区别

三种刷新操作
  • 正常操作:地址栏输入url ,跳转链接,前进后退等 强制缓存有效,协商缓存有效
  • 手动刷新: F5 , 点击刷新按钮,右击菜单刷新 强制缓存失效,协商缓存有效
  • 强制刷新: ctrl + F5 强制缓存失效,协商缓存失效

本地存储

Cookie

最早被提出来的本地存储方式,在每一次 http 请求携带 Cookie,可以判断多个请求是不是同一个用户发起的,特点是:

  • 有安全问题,如果被拦截,就可以获得 Session 所有信息,然后将 Cookie 转发就能达到目的
  • 每个域名下的Cookie不能超过20个,大小不能超过4kb
  • Cookie在请求新页面的时候都会被发送过去
  • Cookie创建成功名称就不能修改
  • 跨域名不能共享Cookie

如果要跨域名共享Cookie有两个方法

  • 用 Nginx 反向代理
  • 在一个站点登录之后,往其他网站写 Cookie。服务端的 Session 存储到一个节点,Cookie 存储 SessionId

Cookie的使用场景

  • 最常见的就是 Cookie 和 Session 结合使用,将 SessionId 存储到 Cookie 中,每次请求都会带上这个 SessionId 这样服务端就知道是谁发起的请求
  • 可以用来统计页面的点击次数

Cookie都有哪些字段

  • NameSize 名字大小
  • Value:保存用户登录状态,应该将该值加密,不能使用明文
  • Path:可以访问此 Cookie 的路径。比如 juejin.cn/editor ,path是/editor,只有/editor这个路径下的才可以读取 Cookie
  • httpOnly:表示禁止通过 JS 访问 Cookie,减少 XSS 攻击。
  • Secure:只能在 https 请求中携带
  • SameSite:规定浏览器不能在跨域请求中携带 Cookie 减少 CSRF 攻击
  • Domain:域名,跨域或者 Cookie 的白名单,允许一个子域获取或操作父域的Cookie,实现单点登录的话会非常有用
  • Expires/Max-size:指定时间或秒数的过期时间,没设置的话就和 Session 一样关闭浏览器就失效
LocaStorage
  • 是H5的新特性,是将信息存储到本地,它的存储大小比 Cookie 大得多,有5M,而且是永久存储,除非主动清理,不然会一直存在
  • 受到同源策略限制,就是端口、协议、主机地址有任何一样不同都不能访问,还有在浏览器设为隐私模式下,也不能读取 LocalStorage
  • 它的使用场景就很多了,比如存储网站主题、存储用户信息、等等,存数数据量多或者不怎么改变的数据都可以用它
SessionStorage
  • SessionStorage 也是H5新特性,主要用于临时保存同一窗口或标签页的数据,刷新页面时不会删除,但是关闭窗口或标签页之后就会删除这些数据
  • essionStorage 和 LocalStorage 一样是在本地存储,而且都不能被爬虫爬取,并且都有同源策略的限制,只不过 SessionStorage 更加严格,只有在同一浏览器的同一窗口下才能共享
  • 它的 API 和 LocalStorage 也一样 getItem、setItem、removeItem、clear
  • 它的使用场景一般是具有时效性的,比如存储一些网站的游客登录信息,还有临时的浏览记录等
indexDB

是浏览器本地数据库,有以下特点

  • 键值对储存:内部用对象仓库存放数据,所有类型的数据都可以直接存入,包括js对象,以键值对的形式保存,每条数据都有对应的主键,主键是唯一的
  • 异步:indexDB操作时用户依然可能进行其他操作,异步设计是为了防止大量数据的读写,拖慢网页的表现
  • 支持事务:比如说修改整个表的数据,修改了一半的时候报了个错,这时候会全部恢复到没修改之关的状态,不存在修改一半成功的情况
  • 同源限制:每一个数据库应创建它对应的域名,网页只能访问自身域名下的数据库
  • 存储空间大:一般来说不少于250MB,甚至没有上限
  • 支持二进制存储:比如ArrayBuffer对象和Blob对象

前端存储方式除了上面四个,还有WebSQL,类似于SQLite,是真正意义上的关系型数据库,可以使用sql进行操作,只是用js时要进行转换,比较麻烦

上面四个的区别

image-20211204173958655

离线存储

Service Worker

Service Worker是运行js主线程之外的,在浏览器背后的独立线程,自然也无法访问DOM,它相当于一个代理服务器,可以拦截用户发出的请求,修改请求或者直接向用户发出回应,不用联系服务器。比如加载JS和图片,这就让我们可以在离线的情况下使用网络应用

一般用于离线缓存(提高首屏加载速度)、消息推送网络代理等功能。使用Service Worker的话必须使用https协议,因为Service Worker中涉及到请求拦截,需要https保障安全

用Service Worker来实现缓存分三步:

  • 一是注册
  • 然后监听install事件后就可以缓存文件
  • 下次再访问的时候就可以通过拦截请求的方式直接返回缓存的数据
// index.js 注册
if (navigator.serviceWorker) { 
    navigator.serviceWorker.register('sw.js').then( registration => {
        console.log('service worker 注册成功')
    }).catch((err)=>{
        console.log('servcie worker 注册失败')
    })
} 
// sw.js  监听 `install` 事件,回调中缓存所需文件 
self.addEventListener('install', e => {
    // 打开指定的缓存文件名
    e.waitUntil(caches.open('my-cache').then( cache => {
        // 添加需要缓存的文件
        return cache.addAll(['./index.html', './index.css'])
    }))
})
// 拦截所有请求事件 缓存中有请求的数据就直接用缓存,否则去请求数据 
self.addEventListener('fetch', e => { 
    // 查找request中被缓存命中的response
    e.respondWith(caches.match(e.request).then( response => {
        if (response) {
            return response
        }
        console.log('fetch source')
    }))
})