菜单

Q
发布于 2025-06-11 / 2 阅读
0
0

初识前端性能优化(应用篇)

优化首屏体验——Lazy-Load初探

Lazy-Load是什么

Lazy-Load,翻译过来是“懒加载”。它是针对图片加载时机的优化:在一些图片量比较大的网站(比如电商网站首页,或者团购网站、小游戏首页等),如果我们尝试在用户打开页面的时候,就把所有的图片资源加载完毕,那么很可能会造成白屏、卡顿等现象,因为图片真的太多了,一口气处理这么多任务,浏览器做不到啊!

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

Lazy-Load的实现

<!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
      <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>

在懒加载的实现中,有两个关键的数值:一个是当前可视区域的高度,另一个是元素距离可视区域顶部的高度

当前可视区域的高度, 在和现代浏览器及 IE9 以上的浏览器中,可以用 window.innerHeight 属性获取。在低版本 IE 的标准模式中,可以用 document.documentElement.clientHeight 获取,这里我们兼容两种情况:··

const viewHeight = window.innerHeight || document.documentElement.clientHeight 

元素距离可视区域顶部的高度,我们这里选用 getBoundingClientRect() 方法来获取返回元素的大小及其相对于视口的位置。对此 MDN 给出了非常清晰的解释:

该方法的返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合, 即:是与该元素相关的 CSS 边框集合 。
DOMRect 对象包含了一组用于描述边框的只读属性——left、top、right 和 bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。

top 属性代表了元素距离可视区域顶部的高度,正好可以为我们所用!

Lazy-Load 方法开工啦!

<script>
    // 获取所有的图片标签
    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].getBoundingClientRect().top
            // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
            if(distance >= 0 ){
                // 给元素写入真实的src,展示图片
                imgs[i].src = imgs[i].getAttribute('data-src')
                // 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
                num = i + 1
            }
        }
    }
    // 监听Scroll事件
    window.addEventListener('scroll', lazyload, false);
</script>

事件的节流(throttle)与防抖(debounce)

节流与防抖的本质

这两个东西都以闭包的形式存在。

它们通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

节流Throttle:第一个人说了算

throttle 的中心思想在于:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。

//第一个人说了算 节流
//定义一个函数,这个函数会传入需要执行的函数,以及执行的间隔时长。
function throttle(fn, interval) {
  //函数内部定义一个最后执行的时间,初始化为 0;
  let lastTime = 0;
  //定义一个_throttle 函数,并返回这个函数,这个函数就是事件触发时需要执行的函数,比如点击事件。
  const _throttle = function (...args) {
    const nowTime = new Date().getTime();
    //通过闭包的技术,获取到这个_throttle 函数执行的时间,
    const remainTime = interval - (nowTime - lastTime);
    if (remainTime <= 0) {
      //如果执行的间隔小于当次执行时间nowTime和上一次执行时间lastTime的间隔,那么就执行传入的函数,并更改上一次执行时间(lastTime)。
      fn.apply(this, args);
      lastTime = nowTime;
    }
  };
  return _throttle;
}

防抖Debounce:最后一个人说了算

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

//定义一个函数debounce,这个函数会传入一个会被执行的方法,和防抖的时长
function debounce(fn, delay) {
  //通过闭包的技术,在外部先定义一个 timer,然后执行防抖函数时先判断 timer 函数存在不存在,存在就无效化定时器。
  let timer = null;
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer);
    //创建一个定时器,并赋值给 timer 变量,这是为了避免 this 指向被改变,所以我们需要使用 apply 来制定 this
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
  //返回一个函数,这个函数就是处理好的防抖函数,可以传入被执行的函数所需要的参数
  return _debounce;
}

用节流Throttle来优化防抖Debounce

debounce 的问题在于它“太有耐心了”。试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。

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

/**
 * 创建一个带有最大等待时间的防抖函数。
 * 在高频触发下,它既能像普通防抖一样推迟执行,
 * 又能保证在指定的最大等待时间(maxWait)后至少执行一次。
 *
 * @param {Function} func 需要防抖的函数。
 * @param {number} wait 延迟执行的毫秒数。
 * @param {number} maxWait 允许延迟执行的最长毫秒数。
 * @returns {Function} 返回新的防抖函数。
 */
function debounceMax(func, wait, maxWait) {
  let timeoutId = null;
  let lastArgs = null;
  let lastThis = null;
  let result = null;
  let lastInvokeTime = 0; // 上次执行 func 的时间戳

  // 确保 wait 和 maxWait 是数字
  wait = Number(wait) || 0;
  maxWait = Number(maxWait) || 0;

  // 真正执行函数的地方
  function invokeFunc() {
    const time = Date.now();
    lastInvokeTime = time; // 记录本次执行时间
    result = func.apply(lastThis, lastArgs);
    
    // 执行后清空,以便垃圾回收
    lastThis = null;
    lastArgs = null;
  }
  
  // 定时器到期后调用的函数
  function timerExpired() {
    timeoutId = null; // 定时器已触发,清空ID
    
    // 如果有最后一次的调用信息,则执行
    if (lastArgs) {
        invokeFunc();
    }
  }

  // 返回给用户使用的函数
  const debounced = function(...args) {
    const time = Date.now();
    lastArgs = args; // 保存最新的参数
    lastThis = this; // 保存最新的 this 上下文

    // 如果这是第一次触发,或者距离上次执行已经超过了 maxWait
    if (lastInvokeTime === 0) {
      lastInvokeTime = time;
    }
    
    // 计算距离上次执行 func 过去了多久
    const timeSinceLastInvoke = time - lastInvokeTime;

    // 清除之前设置的定时器,这是“防抖”的核心
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
    
    // 检查是否满足最大等待时间,如果满足则立即执行
    if (timeSinceLastInvoke >= maxWait && maxWait > 0) {
      invokeFunc();
    } else {
      // 否则,设置一个新的定时器,在 wait 时间后执行
      timeoutId = setTimeout(timerExpired, wait);
    }

    return result;
  };

  return debounced;
}


评论