前端性能优化

时间:2022-07-25
本文章向大家介绍前端性能优化,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

之前写过一篇文章前端网络高级篇(六)网站性能优化,里面提到过13个性能优化的点:

  • 减少HTTP请求
  • 使用CDN
  • 利用HTTP缓存
  • Gzip压缩
  • 将样式表放在顶部
  • 将JS脚本放在底部
  • 避免CSS表达式
  • 使用外部JS和CSS
  • 减少DNS查找
  • 压缩JavaScript和CSS
  • 少用iframe
  • JS文件异步/按需加载
  • 图片懒加载

在具体编程方面,再补充几个点。

1. DOM编程优化

用JS操作DOM,是比较慢的。为什么呢?首先,补充一下浏览器相关的知识。下图为浏览器结构:

image

我们只要关心渲染引擎(Rendering engine)和JS引擎(JavaScript Interpreter)也可称为JS解析器。如图所示,当用JS引擎和渲染引擎是独立实现的,两者通过桥接接口通信。而DOM由渲染引擎绘制,所以,当JS改变DOM结构时,必须通过Bridge通知给渲染引擎,然后进行重排或者重绘。这个通信是有开销的。

重排:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。

重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式。

重排的开销要远大于重绘

所以,我们的优化点就是:

  1. 尽可能减少DOM操作
  2. 尽可能减少重排

看看下面的例子吧。

例子1: 在container元素里面添加10000个“hello”。

不好的行为(JS多次读取DOM元素):

for(var count=0;count<10000;count++){ 
  document.getElementById('container').innerHTML+='<span>hello</span>';
}

改造(JS只读取一次DOM元素,但是,依旧多次更改DOM元素):

// 只获取一次container
let container = document.getElementById('container');
for(let count=0;count<10000;count++){ 
  container.innerHTML += '<span>hello</span>';
}

再改造(JS只读取一次DOM元素,只操作DOM元素):

let container = document.getElementById('container');
let content = '';
for(let count=0;count<10000;count++){ 
  // 拼接内容
  content += '<span>hello</span>';
} 
// 最后更改DOM
container.innerHTML = content;

其实,JS里面用+号拼接String开销也略大,一般会建议创建数组,然后通过array.join('')将数组转为String,如:

let array = [];
for(let count=0;count<10000;count++){ 
  // 拼接内容
  array.push('<span>hello</span>');
} 
container.innerHTML = array.join('');

不过,DOM提供了更好的内置容器来帮助做内容拼接 - DOM Fragment。最后,用 DOM Fragment 改写总结版:

let container = document.getElementById('container');
// 创建一个DOM Fragment对象作为容器
let content = document.createDocumentFragment();
for(let count=0;count<10000;count++){
  // 通过DOM API创建span
  let spanElt = document.createElement("span");
  spanElt.innerHTML = 'hello';
  // 像操作真实DOM一样操作DOM Fragment对象
  content.appendChild(spanElt);
}
// 最后更改DOM
container.appendChild(content)
例子2:更改DOM元素样式

不好的行为(逐条更改样式):

const container = document.getElementById('container')
container.style.width = '100px';
container.style.height = '200px';
container.style.color = 'red';

改造(利用class,只改动一次样式):

//style.css
.basic_style {
   width: 100px;
   height: 200px;
   color: red;
}
//app.js
const container = document.getElementById('container');
container.classList.add('basic_style');

当DOM离线时(display: none),无论怎么操作,浏览器都不会绘制它,也就不会引发重排或者重绘。所以,利用这个特性再改造一版:

let container = document.getElementById('container');
container.style.display = 'none';
container.style.width = '100px';
container.style.height = '200px';
container.style.color = 'red';

container.style.display = 'block'

最后提醒一下,下面的属性慎用。因为这些属性都需要实时计算得到,所以,浏览器为了取得正确的值,会进行重排

offsetTop、offsetLeft、 offsetWidth、offsetHeight、 scrollTop、scrollLeft、scrollWidth、scrollHeight、 clientTop、clientLeft、clientWidth、clientHeight

3. 事件节流(throttle)和防抖(debounce)

比如窗口的scroll和resize事件,一旦激活,会频繁触发相应的事件函数。频繁触发回掉函数导致的大量计算有可能引发页面抖动甚至卡顿。为了规避这些风险,我们会采用事件节流或者防抖,来降低函数的触发频率。

节流:当事件第一次被触发时,在指定时间内,无论再次触发多少次,都会被忽略。也就是说,以第一次事件为准。 防抖:事件触发后,会延迟执行,在延迟时间内,如果事件再次被触发,上一次的事件被取消,以当次为准,重新延迟执行。也就是说,以最后一次事件为准。

示例代码如下:

// 节流 1
let canRun = true;
$(window).scroll(() => {
   if(!canRun){
       // 判断是否已空闲,如果在执行中,则直接return
        return;
   } 
   canRun = false;
    setTimeout(() => {
        canRun = true;
    }, 300);
}); 

// 节流 2
let interval = 300;
let last = 0;
$(window).scroll(() => {
   let now = +new Date()
   if (now - last >= interval) {
      // 如果时间间隔大于设定的时间间隔阈值,则执行回调
      last = now;
      ....
    }
}); 

// 防抖
let timer;
$(window).scroll(() => {
  if(timer){
    clearTimeout(timer)
  }
  timer = setTimeout(() => {
    // 延时 200ms,处理滚动逻辑
  }, 200)
})

一般在浏览器scroll和resize事件应用节流,在远程搜索场景下,应用防抖。

4. CSS优化

CSS选择器是从右向左解析的,所以,尽可能直接用class作为选择器,减少查询时间。

// 推荐
.top {...}
// 不推荐
// 浏览器会先查找所有的a标签,然后再找这些a标签中哪些有span父标签...
div span a {...}