JS 优化

JS 优化

本文介绍常见的优化 JS、提升 JS 加载性能的优化方法!

提升加载性能

script 放入到 body 中

<script> 标签经常以下面的这种方式引入:

<script src="script.js"></script>

当 HTML 解析器看到这一行代码时,就会请求获取脚本,并执行脚本。一旦这个过程完成,解析就可以继续,剩下的 HTML 也可以被分析。所以你可以想象,这个操作会对页面的加载时间产生多么大的影响。如果脚本加载的时间比预期的稍长,例如,如果网络有点慢,或者如果您在移动设备上,并且网速特别慢,则在加载和执行脚本之前,访问者可能会看到一个空白页。

所以推荐将 script 标签从 <head> 位置挪到 </body> 标签前。如果你这样做了,脚本在所有页面都被解析和加载之后才会加载和执行,这是对 <head> 替代方案的巨大改进。

<script defer src="script.js"></script>

Async 和 Defer

如果不考虑兼容旧浏览器,那么 asyncdefer 这两个布尔属性值,会是提升页面加载速度的更好选择:

<script async src="script.js"></script>
<script defer src="script.js"></script>

这两个属性都可以达到异步加载和执行 script 标签的目的,如果同时指定了两个,那么 async 优先级高一点,老一点的浏览器不支持 async 会降级到 defer。这些属性只有在页面的 <head> 部分使用 <script> 时才有意义,如果像我们上面看到的那样将脚本放在 <body> 中,则这些属性是无用的。

使用 async 会阻塞 HTML 的解析:

使用 defer 并不会阻塞 HTML 的解析:

因此在使用 script 时,要加快页面加载速度,最好的方法是将它们放在 <head> 中,并在 <script> 中添加一个 defer 属性:

<script defer src="script.js"></script>

JS 变量和函数优化

尽量使用 ID 选择器

使用 ID 选择器来选择元素永远都是最快的!

不要使用 eval

eval 函数的三宗罪:

  • 不正确使用 eval 会让代码变得更容易注入攻击
  • 调试可能更具挑战性(没有行号等)
  • eval 代码执行速度较慢(没有机会编译/缓存 eval 代码)

JS 事件节流

假设您有一个滚动事件处理程序,当用户在页面上向下移动时,您想在其中向用户显示新内容。如果我们在用户每次滚动单个像素时就执行回调,那么如果他们快速滚动事件,我们的页面将会变得巨卡无比,因为它将快速连续发送数百或数千个事件。相反,我们对其进行限制,比如仅检查每100毫秒滚动一次的数量,这样每秒仅获得10个回调。用户仍然可以立即感觉到响应,并且计算效率更高。可以看到,通过限制这些回调或者比如频繁的 API 请求等,可以防止应用程序卡住或对服务器不必要的请求。

节流函数示例:

// Pass in the callback that we want to throttle and the delay between throttled events
const throttle = (callback, delay) => {
  // Create a closure around these variables.
  // They will be shared among all events handled by the throttle.
  let throttleTimeout = null;
  let storedEvent = null;

  // This is the function that will handle events and throttle callbacks when the throttle is active.
  const throttledEventHandler = event => {
    // Update the stored event every iteration
    storedEvent = event;

    // We execute the callback with our event if our throttle is not active
    const shouldHandleEvent = !throttleTimeout;

    // If there isn't a throttle active, we execute the callback and create a new throttle.
    if (shouldHandleEvent) {
      // Handle our event
      callback(storedEvent);

      // Since we have used our stored event, we null it out.
      storedEvent = null;

      // Create a new throttle by setting a timeout to prevent handling events during the delay.
      // Once the timeout finishes, we execute our throttle if we have a stored event.
      throttleTimeout = setTimeout(() => {
        // We immediately null out the throttleTimeout since the throttle time has expired.
        throttleTimeout = null;

        // If we have a stored event, recursively call this function.
        // The recursion is what allows us to run continusously while events are present.
        // If events stop coming in, our throttle will end. It will then execute immediately if a new event ever comes.
        if (storedEvent) {
          // Since our timeout finishes:
          // 1. This recursive call will execute `callback` immediately since throttleTimeout is now null
          // 2. It will restart the throttle timer, allowing us to repeat the throttle process
          throttledEventHandler(storedEvent);
        }
      }, delay);
    }
  };

  // Return our throttled event handler as a closure
  return throttledEventHandler;
};

如何使用这个节流器呢?

var returnedFunction = throttle(function() {
  // Do all the taxing stuff and API requests
}, 500);

window.addEventListener('scroll', returnedFunction);

事件委托

如下图所示,“+ Add Character” 按钮可以动态地给 DOM 增加复选框,复选框本身又可以进行点击,那么新增的复选框点击的事件如何绑定上?针对此问题,事件委托的解决方案是指将拦截点击的事件监听器绑定在父元素 <ul> 上,而非单个 input 上。

最好把事件委托看作是负责任的父母和疏忽大意的孩子。父母基本上是神,孩子们必须听父母的话。美妙的是,如果我们增加更多的孩子(更多的输入),父母会保持不变-他们从一开始就在那里(ul 元素在页面加载的时候就存在)。

事件委托的代码:

<ul class=”characters”> // PARENT -- 监听器绑定在这个元素上 !
 <li>
   <input type=”checkbox” data-index=”0" id=”char0"> //CHILD 1
   <label for=”char0">Mickey</label>
 </li>
 
 <li>
   <input type=”checkbox” data-index=”1" id=”char1"> //CHILD 2
   <label for=”char1">Minnie</label>
 </li>
 
 <li>
   <input type=”checkbox” data-index=”2" id=”char2"> //CHILD 3
   <label for=”char2">Goofy</label>
 </li>
</ul>
<script>
//Event Delegation
function toggleDone (event) {
  if (!event.target.matches(input)) return
  console.log(event.target)
  //We now have the correct input - we can manipulate the node here
}

  const characterList = document.querySelector('.characters');
  characterList.addEventListener('click', toggleDone);
</script>

上述,当点击任意一个 input 的时候,event.target 的值指向的是哪个元素发生了点击:

event.currentTarget 代表的是事件监听器绑定在哪个元素上:

更进一步,当点击 input 元素的时候,这个事件会沿着自己的父链向上传递,所以任何一个父节点都能感受到这个事件,这称之为事件冒泡

如果没有事件委托,则必须将单击事件监听器重新绑定到加载到页面的每个新输入,这是复杂和繁重的编码任务。首先,它会大幅增加页面上事件监听器的数量,更多的事件监听器会增加页面的总内存占用。内存占用量越大,性能越差……这是一件坏事。其次,可能存在与绑定和解除绑定事件监听器以及从 DOM 中删除元素相关联的内存泄漏问题。

JS 动画优化

尽量使用 CSS 动画

如果 CSS 动画能够满足要求,那么尽量使用 CSS 动画,因为其可以充分利用浏览器提供的优化、甚至可以使用 GPU 来提高性能。而 JS 动画涉及到导入库、JS 执行、学习 JS 动画库提供的 API 等,成本高。

使用 requestAnimationFrame

  • 浏览器可以优化它,所以动画看起来更流畅
  • 当页面不可见的时候,动画自动停止运行,让 CPU 歇会儿
  • 比较省电

JavaScript 函数应该保持简洁

将大量 JS 动画添加到页面中,一定要想好。如果你的页面开始变卡,代码看起来不太灵活,那么可以使用 Web Workers 来尝试将 JS 动画放到另外一个线程来执行。

参考

扫描下面二维码,在手机端阅读: