探讨如何通过优化JavaScript代码提高性能的实践学习 | 青训营

100 阅读9分钟

在浏览器中, JavaScript 负责实现动态交互和复杂业务,因此 JavaScript 代码的性能会影响页面加载速度和用户体验性,所以对开发而言,代码的优化是必要的。下面将围绕减少重绘和重排、使用节流和防抖技术、使用性能分析工具等方面实践学习性能优化和调试技巧。

减少重绘和重排

重绘(repaint)和重排(reflow)是浏览器渲染页面的两个过程,这会消耗大量资源、时间,从而影响页面的流畅度和响应速度等。重绘指当元素的外观发生变化,但不影响布局时,浏览器重新绘制元素的过程。例如改变元素的颜色、背景、边框等。重排指当元素的尺寸、位置或者显示状态发生变化,导致布局发生变化时,浏览器重新计算元素的几何属性并重新布局页面的过程。例如改变元素的宽高、边距、定位、显示隐藏等。
重排一定会引起重绘,但重绘不一定引起重排。重排比重绘更耗性能,因为会影响后续元素的布局和渲染。减少重绘和重排是必要的,可以采取以下措施:

减少频繁地修改元素的样式属性,特别是影响布局的属性。若需要修改多个属性,可用requestAnimationFramesetTimeout将修改操作放在一次回调函数中执行,或者使用documentFragmentdisplay:none先将元素从文档流中移除,再进行批量修改,最后再插入文档流中;
减少使用表格布局,表格中的任何元素发生变化都会导致整个表格重新布局;
减少使用CSS表达式,它会在每次页面渲染时重新计算值;
尽量用transformopacityfilter等不影响布局的属性来实现动画效果,而不是用lefttopwidthheight等;
尽量避免在低层级的元素上使用z-index属性,因为它会创建一个新的层叠上下文,导致该元素及其子元素重新渲染;

实践例子:使用 window.requestAnimationFrame 函数减少重绘和重排

image.png image.png

// 获取需要动画的元素
var box = document.getElementById("box");
// 定义旋转角度
var angle = 0;
// 定义回调函数
function rotate() {
  // 增加旋转角度
  angle += 1;
  // 如果超过360度,归零
  if (angle > 360) {
    angle = 0;
  }
  // 使用transform属性来实现旋转效果,不影响布局
  box.style.transform = "rotate(" + angle + "deg)";
  // 使用requestAnimationFrame来请求下一次动画帧,避免频繁修改样式属性
  requestAnimationFrame(rotate);
}
// 调用requestAnimationFrame开始动画
requestAnimationFrame(rotate);

image.png

使用节流和防抖技术

节流(throttle)和防抖(debounce)是两种常用的优化技术,它们可减少函数的执行频率,从而提高性能。节流是指在一定时间间隔内,只执行函数的第一次或最后一次调用,忽略中间的调用。比如使用节流来优化窗口的resize事件或者鼠标的mousemove事件,避免在短时间内触发过多的回调函数。防抖是指在一定时间间隔内,只执行函数的最后一次调用,取消前面的调用。比如使用防抖来优化输入框的keyup事件或者按钮的click事件,避免在用户输入或点击过程中触发过多的回调函数。
实现节流和防抖,可使用以下方法:

使用setTimeoutclearTimeout来控制函数的执行时间。比如在函数开始时设置一个定时器,在定时器到期后执行函数,并清除定时器;或者我们可以在函数结束时设置一个定时器,在定时器到期后执行函数,并取消前面的定时器;
使用Date.now()或者performance.now()来获取当前时间戳,并与上一次执行时间进行比较。比如在函数开始时获取当前时间戳,并与上一次执行时间进行比较,如果超过了设定的时间间隔,则执行函数,并更新上一次执行时间;或者在函数结束时获取当前时间戳,并与上一次执行时间进行比较,如果没有超过设定的时间间隔,则延迟执行函数,并更新上一次执行时间;
使用第三方库或者工具来实现节流和防抖功能。例如,使用lodash、underscore等库中提供的_.throttle_.debounce方法来简化代码;或使用Chrome DevTools中提供的Throttling和Debounce工具来模拟网络延迟和用户输入;

实践例子:使用定时器函数

image.png image.png

// 使用setTimeout和clearTimeout来实现节流
function throttle(func, delay) {
  // 定义一个变量来存储定时器
  var timer = null;
  // 返回一个新的函数
  return function () {
    // 获取函数的执行上下文和参数
    var context = this;
    var args = arguments;
    // 如果定时器不存在
    if (!timer) {
      // 设置一个定时器,在delay毫秒后执行函数,并清除定时器
      timer = setTimeout(function () {
        func.apply(context, args);
        timer = null;
      }, delay);
    }
  };
}
// 使用setTimeout和clearTimeout来实现防抖
function debounce(func, delay) {
  // 定义一个变量来存储定时器
  var timer = null;
  // 返回一个新的函数
  return function () {
    // 获取函数的执行上下文和参数
    var context = this;
    var args = arguments;
    // 如果定时器存在,取消前面的定时器
    if (timer) {
      clearTimeout(timer);
    }
    // 设置一个新的定时器,在delay毫秒后执行函数,并赋值给定时器
    timer = setTimeout(function () {
      func.apply(context, args);
    }, delay);
  };
}

// 使用节流和防抖来优化窗口的resize事件和输入框的keyup事件
// 定义一个处理窗口resize事件的函数,打印窗口的宽度和高度
function handleResize() {
  console.log(window.innerWidth, window.innerHeight);
}
// 定义一个处理输入框keyup事件的函数,打印输入框的值
function handleKeyup() {
  console.log(document.getElementById("input").value);
}
// 获取窗口对象和输入框对象
var window = document.getElementById("window");
var input = document.getElementById("input");

// 给窗口对象添加resize事件监听,使用节流来控制函数的执行频率,设置时间间隔为500毫秒
window.addEventListener("resize", throttle(handleResize, 500));
// 给输入框对象添加keyup事件监听,使用防抖来控制函数的执行频率,设置时间间隔为500毫秒
input.addEventListener("keyup", debounce(handleKeyup, 500));

节流可以保证在一定时间间隔内至少执行一次函数,避免因为频繁触发事件而导致页面卡顿或响应延迟。防抖可以保证在一定时间间隔内只执行一次函数,避免因为重复触发事件而导致不必要的计算或者请求。节流和防抖都可以减少资源的浪费和网络的拥堵,提高页面性能和用户体验。

使用性能分析工具

性能分析工具可以帮助我们检测和优化 JavaScript 代码的性能,它们具有的功能包括:1、记录和展示代码的执行过程和时间,如主线程的事件循环、每个事件循环的任务、每个任务的调用栈、每个函数的耗时等;2、定位和高亮代码中的性能瓶颈和问题,如内存泄露、过多的重绘和重排、低效的循环和函数等;3、提供一些优化建议和解决方案,如使用缓存、节流和防抖、减少全局变量、使用直接量等。

实践学习: Chrome DevTools 的 Performance 工具,F12打开即用。
Chrome DevTools 的 Performance 工具是 Chrome 浏览器内置的一个强大的工具,可以记录和展示网页的性能数据,包括JS代码、CSS样式、DOM操作、网络请求等。它还可以模拟不同的网络和CPU条件,以及提供一些优化建议。

Chrome DevTools 的 Performance 工具主要包括以下部分:
概览部分:显示了整体的界面渲染情况,每个时间段执行的事件顺序,以及一些关键指标,如页面帧速(FPS)、CPU 资源消耗、网络请求流量、V8 内存使用量(堆内存)等;
性能面板部分:显示了渲染进程中各个线程的执行记录,包括主线程(Main)、合成线程(Compositor)、光栅化线程池(Raster)、GPU进程主线程(GPU)等。每个线程中可以看到不同类型的任务,如脚本(Scripting)、渲染(Rendering)、绘制(Painting)、系统(System)等。每个任务中可以看到不同层级的函数调用栈和耗时;
性能摘要部分:显示了在检测性能的时间范围内,各个类型任务所占用的时间比例和总时长,如加载(Loading)、脚本(Scripting)、渲染(Rendering)、绘制(Painting)、其他(Other)、空闲(Idle)等;
性能细节部分:显示了在检测性能的时间范围内,一些关键的时间节点在何时产生的数据信息,如首次内容绘制(FCP)、最大内容绘制(LCP)、首次有意义绘制(FMP)、DOM内容加载事件(DCL)、页面加载事件(L)等;
底部信息栏部分:显示了当前选中的任务或函数的详细信息,如名称、耗时、文件位置等;

image.png

使用 Chrome DevTools 的 Performance 工具的步骤:
1、点击左上角的黑色按钮或者圆形箭头按钮来触发性能数据记录,黑色按钮可以记录交互阶段的性能数据,圆形箭头按钮用来记录加载阶段的性能数据;
2、点击右上角的停止按钮或者等待自动停止来结束性能数据记录;
3、查看并分析性能数据,可以选择不同的视图模式和过滤条件,也可以放大和缩小时间范围;
4、定位并解决性能问题,可以查看高亮或者标记的部分,并点击跳转到源码位置。 image.png image.png

总结

JavaScript 代码的性能会影响页面加载速度和用户体验性,所以对开发而言,代码的优化是必要的,本次主要围绕学习了三种方式来优化 JavaScript 代码而提高性能,包括减少重绘和重排、使用节流和防抖技术和使用性能分析工具。在学习过程中,也发现还有更多优化 JavaScript 代码的方式,所以自己还需要继续积累和研究。