前端性能优化-代码篇| 青训营笔记

131 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第10天

关于优化

sleep(1000)->sleep(920

我们每天都在谈前端性能优化,每天都在背前端性能优化方案,然而,我们却不知道其背后的原理以及涉及那些知识储备。不妨,问自己一些问题:

  1. 我们为什么要做前端性能优化?
  2. 性能优化的标准是什么?
  3. 我们应该从那些角度去谈性能优化?

项目的性能决定了用户对项目的整体感观度,优秀的性能可以保证项目的流畅与自然给用户愉快的体验感受。当我们开发完成一个项目以后最重要的环节就是要保证项目拥有一个良好的性能,所以开发完成以后需要我们针对项目做好性能优化。

前端性能优化可以从这么几个角度谈起:

  • 代码优化
  • 渲染优化
  • 资源优化
  • 传输加载优化
  • 构建优化
  • 其他优化

优化标准

核心指标是我们页面做性能测试时必不可少的几个指标。

FCP(First Contentful Paint)首次内容绘制

指浏览器从响应用户输入网络地址到页面内容的任何部分在屏幕上完成渲染的时间。这个就是实际有意义的首屏时间。

其实这里还有一个核心指标:FP(First Paint)首次绘制,但正常情况下做FCP就没必要做FP了。

LCP(Largest Contentful Paint)最大内容绘制

表示可视区最大内容(文本块或图像元素)在屏幕上完成渲染的时间。该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录。

TTI(Time to Interactive)可交互时间

测量页面从开始加载到视觉上完成渲染、初始脚本完成加载,并能够快速、可靠地响应用户输入所需的可交互状态时间。 可交互状态指的是页面上的 UI 组件是可以交互的(可以响应按钮的点击或在文本框输入文字等)。

TBT(Total Blocking Time)总阻塞时间

指 FCP(首次内容绘制) 与 TTI (可交互时间)之间的总时间。这期间,主线程被阻塞的时间过长,无法作出输入响应。举个例子说明一下:

主线程上的任务时间轴

上图为页面加载期间浏览器主线程的图表, 上方的时间轴上有五个任务,其中三个是长任务,因为这些任务的持续时间超过50毫秒。下图显示了各个长任务的阻塞时间:

显示阻塞时间的主线程任务时间轴

因此,虽然在主线程上运行任务的总时间为 560 毫秒,但其中只有 345 毫秒被视为阻塞时间。

CLS (Cumulative Layout Shift) 累积布局偏移

累积布局偏移 (CLS) 是一个以用户为中心的测量视觉稳定性的重要指标,因为该项指标有助于量化用户发生意外布局偏移的频率,较低的 CLS 有助于用户的使用体验。 如果还不知道什么是CLS的话找了个图可以看着理解一下:

未标题-1.gif 解释一下就是想点取消,但因为别内容加载出来把按钮顶下去了,所以点成了确定订单。大写的尴尬!!

注意:2021年6月1日:CLS 的实现方式已发生变更。想了解具体的变更原因可以查看 不断发展 CLS 指标

以前 CLS 测量的是整个页面生命周期内发生的所有单次布局偏移分数的总和

现在 CLS 测量的是整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数

布局偏移:每当一个可见元素的位置从一个已渲染帧变更到下一个已渲染帧时,就发生了布局偏移 。

一连串的布局偏移:也叫会话窗口,是指一个或多个快速连续发生的单次布局偏移,每次偏移相隔的时间少于 1 秒,且整个窗口的最大持续时长为 5 秒。

最大的一连串:是指窗口内所有布局偏移累计分数最大的会话窗口。

FID(First input delay)首次输入延迟

测量从用户第一次与您的网站交互(例如当他们单击链接、点按按钮或使用由JavaScript驱动的自定义控件)直到浏览器实际能够对交互做出响应所经过的时间。页面的 FID 应为100 毫秒或更短。

上述的几个核心指标其实就是最近几年比较重要的几个核心指标,但核心指标会随着时间的推移而发展。针对 2020 年的指标构成侧重于用户体验的三个方面:加载性能、交互性和视觉稳定性。

2020年的核心Web指标为三大指标:

  • LCP(Largest Contentful Paint)最大内容绘制、
  • FID(First Input Delay)首次输入延迟
  • CLS(Cumulative Layout Shift) 累积布局偏移。

每项指标所测量的用户体验是不同的:

  • LCP 测量感知加载速度,并在页面的主要内容基本加载完成时,在页面加载时间轴中标记出相应的点;
  • FID 测量响应度,并将用户首次尝试与页面交互的体验进行了量化;
  • CLS 测量视觉稳定性,并对可见页面内容的意外布局偏移量进行了量化。

最后就是每项核心Web指标都有一个相关联的阈值,这些阈值将性能分为"良好"、"需要改进"或"欠佳":

企业微信20220716-190224@2x.png

良好欠佳百分位数
最大内容绘制≤2500ms>4000ms75
首次输入延迟≤100ms>300ms75
累积布局偏移≤0.1>0.2575

从代码的角度谈优化

慎用全局变量

  • 全局变量定义在全局执行上下文,是所有作用域链的顶端
  • 全局执行上下文一直存在于上下文执行栈,直到程序退出,消耗内存
  • 如果某个局部作用域出现了同名变量则会遮蔽或者污染全局

缓存全局变量

将使用中无法避免的全局变量缓存到局部

通过原型对象添加附加方法

构造函数、原型对象、实例对象;构造函数和实例对象都可以指向原型对象的,如果某个构造函数内部的成员方法,让后面的实例对象进行频繁的调用,所以我们需要在原型对象上新增实现对象需要的方法
将方法放在原型对象上和构造函数中进行对比!

var fn1=function(){
    this.foo=function(){
        console.log(111)
    }//在其内部,直接给所有的实例对象添加成员方法
}

let f1=new fn1();

var fn2=function(){
    fn2.prototype.foo=function(){
        console.log(111)
    }
}//在原型对象上直接设置函数
let f2=new fn2();

避开闭包陷阱

闭包指的是:能够访问另一个函数作用域中变量的函数。 闭包使用不当很容易出现内存泄漏

  let f = ()=>{
      let func = []
      for(var i = 0; i < 3; i++){
          func.push(()=>{
              return i*i;
          })
      }
      return func 
  }
//这样,f1应该输出0, f2输出1,f3输出4 ……,本应该这样才对。但是结果却是三个9。
  let [f1,f2,f3]=f();
  console.log(f1());  // 9
  console.log(f2());  // 9
  console.log(f3());  // 9

因为使用了var声明的变量。var声明的变量是没有块级作用域的,上面的代码在逻辑上,等价于:

  let f = ()=>{
      let func = []
      var i
      for(i = 0; i < 3; i++){
          func.push(()=>{
              return i*i;
          })
      }
      return func
  }

避免属性访问方法使用

function Person(){
    this.name='icoder';
    this.age=18;
    this.getAge=function(){
        return this.age;//通过添加属性的方法访问
    }
}
const p1=new Person();
const a=p1.getAge();

const p2=new Person();
const b=p2.age;//通过实例直接访问

选择最优的循环方法

foreach>for>forin 19.jpg

节点添加优化(文档碎片)

createDocumentFragment

for(let i=0;i<10;i++){
    var oP=document.createElement('p');
    oP.innerHTML=i;
    document.body.appendChild(oP);
}

const fragEle=document.createDocumentFragment();//创建一个文档碎片容器
for(let i=0;i<10;i++){
    var oP=document.createElement('p');
    oP.innerHTML=i;
    fragEle.appendChild(oP);
}
document.body.appendChild(fragEle);

减少判断层级(易于维护的代码不代表执行速度快)

如果代码中出现多层if-else嵌套的时候,可考虑是否通过提前return的方式,减少嵌套层级,如果发现嵌套中出现大量的else if并且esle if中的值是固定的,则建议使用switch case !

减少作用域链查找层级

每个函数执行时都会产生执行上下文,定义执行的函数环境,当函数执行完成后执行上下文就会被销毁(取决于是否存在闭包),多次调用同一个函数的时候,就会创建多个执行上下文,并且每个上下文都有自己的作用域的,这些作用域之间又可以通过作用域链连接,在函数执行的过程中,每遇到一个变量就会先去搜索自己内部的作用域,如果发现没有就会通过作用域链向上查找(父作用域),如果还没有找到就会进行向上查找。

减少数据读取次数

对象数据有时会在原型链上查找,就需要减少对象查找的次数和属性的嵌套层级——提前将对象的数据进行缓存

字面量与构造式

引用数据类型的字面量和构造式声明差距不大,但基本数据类型字面量和构造式声明差距就非常大 构造式相当于创建了一个对象。

好处就是可以通过原型链直接调用原型链上的方法,字面量声明基本数据类型,在使用一些方法时,是先默认的转换对象,在调用其内部方法,如果没转也是会是对象下的实例,也可以通过原型链找到;

字面量比构造式声明会少占空间,使用构造式声明,需要多余的空间存放对象上面的属性和方法

减少循环体活动(可加快运行时间)

  • 尽可能的将循环体中重复的事情(数据值不变的值)提出出来,在循环体外处理。
  • 采用从后往前遍历,代码量可以减少,减少条件判断,增快循环速度!

减少声明及语句数

  • 不是频繁使用的数据建议直接获取而不是提前进行缓存,降低程序在使用时对内存的消耗
var test=(ele)=>{
    let w=ele.offsetWidth;
    let h=ele.offsetHeight;//需要对ele、w、h解析
    return w+h;
}

var test=(ele)=>{
    return ele.offsetWidth+ele.offsetHeight;//只需要对ele解析
}
  • 最小化语句数(表达式)

采用事件绑定(事件委托)

DOM操作使用事件委托——利用JS事件冒泡机制,把原本需要绑定在子元素的事件委托给了父元素进行监听,可以大量减少内存的占用和事件的注册