前端性能优化

379 阅读11分钟

前端性能调优

1. webpack

1.1 webpack性能

1.1.1构建花费时间
  • 使用exclude避免对node_moudules等大体积文件夹的处理

  • 缓存转译结果

    loader: 'babel-loader?cacheDirectory=true'
    
  • 处理第三方库,如node_modules

    • CommonsChunkPlugin

    • DllPlugin

      基于动态链接的思想创作, 插件会将第三方库单独打包到一个文件中,这是单纯的依赖库

      只有依赖自身发生版本变化时才会重新打包

      //ddl配置
      
      const path = require('path')
      const webpack = require('webpack')
      
      module.exports = {
          entry: {
            // 依赖的库数组
            vendor: [
              'prop-types',
              'babel-polyfill',
              'react',
              'react-dom',
              'react-router-dom',
            ]
          },
          output: {
            path: path.join(__dirname, 'dist'),
            filename: '[name].js',
            library: '[name]_[hash]',
          },
          plugins: [
            new webpack.DllPlugin({
              // DllPlugin的name属性需要和libary保持一致
              name: '[name]_[hash]',
              path: path.join(__dirname, 'dist', '[name]-manifest.json'),
              // context需要和webpack.config.js保持一致
              context: __dirname,
            }),
          ],
      }
      
      //运行配置文件
      //得到
      //vendor-manifest.json
      //vendor.js
      
      
      //webpack.config.js
        plugins: [
          new webpack.DllReferencePlugin({
            context: __dirname,
            // manifest就是我们第一步中打包出来的json文件
            manifest: require('./dist/vendor-manifest.json'),
          })
        ]
      
      
  • Happypack将loader转为多进程

    并发的对文件进行打包

    在该插件中可以手动配置进程数量

1.1.2打包体积大
  • 构建结果压缩

    • treeshaking删除冗余代码

      基于import, export打包工具可以知道那些代码没真正的用到

  • 按需加载

    不要一次加载完所有文件内容,只加载当前使用的

2. 图片

2.1 JPG/JPEG

优点: 有损压缩, 体积小

缺点: 不支持透明度处理

场景:首页大图

2.2 PNG

优点:无损压缩, 高保真,对透明度有良好支持

场景:复杂, 色彩丰富的图片, 如LOGO

2.3 SVG

优点: 文本文件, 体积小, 不失真, 兼容性好, 可压缩性强

缺点:渲染成本高, 有学习成本

2.4 Base64

优点: 小图标解决方案

经典应用: 雪碧图

作用: 减少网络请求

缺点:编码之后体积膨胀,因此适合一些小的图标

场景:

  • 尺寸小
  • 更新频率低

2.5 WebP

优点:支持透明, 可以显示动态图片, 压缩后小

缺点:增加服务器负担

场景:主要考虑是否兼容的问题

3. 缓存机制

3.1 浏览器缓存机制

四个方面

  • Memory Cache
  • Service Worker Cache
  • HTTP Cache
  • Push Cache

3.2 强缓存

利用http头中的Expires与Cache-Control控制

expires为过期时间

Cache-Control通过max-age控制, max-age是一个时间长度,且是相对时间。优先级比expires高

3.3 public与private

针对资源能否被代理服务器缓存而存在的

public表示既可以被浏览器缓存, 也可以被代理服务器缓存

private只能被浏览器缓存

3.4 no-store与no-cache

no-cache跳过浏览器直接询问服务器

nostore不使用缓存策略

3.5 协商缓存

浏览器需要向服务器去查询缓存的相关信息, 进而判断是重新发起请求,下载响应还是本地获取缓存的资源

3.5.1 协商缓存实现

last-modified是一个时间错,如果启用了协商缓存,会在首次请求时随着response header一起返回

之后灭磁请求都会带上一个if-modified-since的时间戳,值为之前的last-modified的值

3.5.2Etag

为了解决服务器没有正确感知文件变化的问题

Etag是为每个资源生成的唯一的标识字符串,这个字符串是基于文件内容的

缺点:会影响服务端的性能

3.6 HTTP缓存决策指南

屏幕截图 2024-10-29 154132.png

3.7 Memory Cache

内存中的缓存,响应速度最快,存储空间有限

3.8 Service Worker Cache

Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。

3.9. Push Cache

是一种存在于会话阶段的缓存

不同页面只要共享同一个HTTP2来凝结,就可以共享同一个Push Cache

4. 本地存储

4.1 Cookie

劣势:

  • 只有4kb
  • 过量的cookie会带来性能浪费

4.2 Web Storage

Web Storage 是 HTML5 专门为浏览器存储而提供的数据存储机制。它又分为 Local Storage 与 Session Storage。

两者区别

  • 生命周期
    • LocakStorage是持久化本地存储,
    • Session Storage是临时性本地存储
  • 作用域
    • Session Storage只要不在同一个浏览器窗口中打开,session Storage的内容无法共享
4.2.1 Web Stroage特性
  • 存储容量大
  • 仅位于浏览器端,不与服务端发生通信
4.2.2 Web Storage核心APi
//存储数据, 注意以键值对的方式存储
localStorage.setItem('user_name', 'xiuyan')

//读取
localStorage.getItem('user_name')

//删除某一键名对应的数据
localStorage.removeItem('user_name')

//清楚数据记录
localStorage.clear()

4.3 indexedDB

IndexedDB 是一个运行在浏览器上的非关系型数据库

理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250M)。它不仅可以存储字符串,还可以存储二进制数据。

5. cdn缓存

CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。

这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响。

6. 服务端渲染SSR

6.1 客户端渲染

服务端发送需要的静态文件, 客户端在浏览器跑js, 根据js运行结果生成相应的dom

6.2 服务端渲染

客户端请求时,由服务器把需要的组件或页面渲染成HTML字符串, 然后把他返回给客户端。

客户端拿到手后直接渲染

6.3 服务端渲染解决了什么性能问题

提高了首屏加载的速度

优化SEO, 因为如果是客户端渲染, 对于某些需要js代码运行后才能添加的关键字,搜索引擎是搜不到的。

7. 浏览器渲染

7.1 浏览器的内核

  • 渲染引擎
    • HTML解释器
    • CSS解释器
    • HTTP引擎
    • 回调函数引擎
  • js引擎

7.2 Chrome的内核Webkit

渲染过程

屏幕截图 2024-10-29 233042.png

模块:

  • HTML解释器
  • CSS解释器
  • 图层布局计算模块
  • 视图绘制模块
  • JavaScript引擎

7.3 渲染过程解析

屏幕截图 2024-10-29 233159.png

  • 解析HTML: 发出页面渲染所需的各种外部资源请求
  • 计算样式:CSS与Dom树结合生成render树

7.4 重要的树

屏幕截图 2024-10-29 233400.png

之后每当一个新元素加入到这个 DOM 树当中,浏览器便会通过 CSS 引擎查遍 CSS 样式表,找到符合该元素的样式规则应用到这个元素上,然后再重新去绘制它。

7.5 CSS样式表优化

  • 避免使用通配符
  • 可以通过继承实现的属性,避免重复匹配
  • 少用标签选择器
  • 减少嵌套,后代选择器的开销最高

7.6 CSS与JS加载顺序

HTML, CSS, JS都具有阻塞渲染的特性

7.6.1 CSS阻塞

发生在解析CSS生成CSSOM树的时候

浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容

因此需要将CSS尽早的下载到客户端

尽早:CSS放在head标签, 尽快:启用CDN实现静态资源加载速度的优化

7.6.2 JS的阻塞

js引擎是独立于渲染引擎存在的。当遇到一个script标签时, 会暂停渲染过程,将控制权交给JS引擎。实际上是JS引擎抢走了渲染引擎的控制权

7.6.3 JS的三种加载方式
  • 正常

    <script src="index.js"></script>
    
  • async

    <script async src="index.js"></script>
    //js的加载时异步的,加载结束时, JS脚本会立即执行
    
  • defer模式

    <script defer src="index.js"></script>
    //js的执行时被推迟的,执行是被推迟的
    

8. DOM优化

js去操作dom时本质上是两个引擎的交互

屏幕截图 2024-10-30 114317.png

  • 回流:当我们对DOM的修改引发了DOM集合尺寸的变化时, 浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来,这个过程就是回流
  • 重绘:对DOM修改样式但未影响几何属性,就不需要重新计算,直接绘制新的样式

重绘不一定导致回流, 回流一定导致重绘

8.1 减少DOM操作

for(var count=0;count<10000;count++){ 
  document.getElementById('container').innerHTML+='<span>我是一个小测试</span>'
} 
//更改为
// 只获取一次container
let container = document.getElementById('container')
for(let count=0;count<10000;count++){ 
  container.innerHTML += '<span>我是一个小测试</span>'
} 
let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){ 
  // 先对内容进行操作
  content += '<span>我是一个小测试</span>'
} 
// 内容处理好了,最后再触发DOM的更改
container.innerHTML = content

核心:用js给DOM分压

9. EventLoop与异步更新策略

9.1 EventLoop

宏任务: SetTimeOut, SetTimeInterval, SetImmediate, script,I/O

微任务:process.nextTick, Promise, MutationObserver

屏幕截图 2024-10-30 141307.png

我们更新 DOM 的时间点,应该尽可能靠近渲染的时机。当我们需要在异步任务中实现 DOM 修改时,把它包装成 micro 任务是相对明智的选择

因为宏任务是一个一个执行,微任务是一队一队执行,的所以如果放在宏任务中会有延迟

9.2 异步更新

当我们去调用框架接口更新数据时,更新不会立刻生效

需要等待一定时机,队列中的更新任务会被批量触发,这就是异步更新

异步更新可以帮助我们避免过度渲染

10. 回流与重绘

回流:

  • 修改DOM的几何属性
  • 改变DOM树的结构
  • 获得一些特定属性的值

如何避免

  • 在js中对一些需要多次获取的值进行一个缓存
  • 避免逐条改变样式,使用类名合并样式

DOM离线,将DOM拿掉

比如display赋值为none

11. 优化首屏

11.1 懒加载

<div data-v-b2db8566="" 
    data-v-009ea7bb="" 
    data-v-6b46a625=""   
    data-src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/9/27/16619f449ee24252~tplv-t2oaga2asx-image.image"    
    class="lazy thumb thumb"    
    //使用none占位
    style="background-image: none; background-size: cover;">  
</div>

实现

关键数值:

当前可视区域的高度

//要考虑浏览器的兼容问题
const viewHeight = window.innerHeight || document.documentElement.clientHeight 

元素距离可视区域顶部的距离

 getBoundingClientRect() 

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>

12. 节流与防抖

频繁触发回调导致的大量计算会引发页面的抖动甚至卡顿。因此

节流(throttle): 在一段时间内不管触发多少次,只会执行一次。

实现

// fn是我们需要包装的事件回调, interval是时间间隔的阈值
function throttle(fn, interval) {
  // last为上一次触发回调的时间
  let last = 0
  
  // 将throttle处理结果当作函数返回
  return function () {
      // 保留调用时的this上下文
      let context = this
      // 保留调用时传入的参数
      let args = arguments
      // 记录本次触发回调的时间
      let now = +new Date()
      
      // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
      if (now - last >= interval) {
      // 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
          last = now;
          fn.apply(context, args);
      }
    }
}

// 用throttle来包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)

document.addEventListener('scroll', better_scroll)

防抖(debounce)

// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
  // 定时器
  let timer = null
  
  // 将debounce处理结果当作函数返回
  return function () {
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments

    // 每次事件被触发时,都去清除之前的旧定时器
    if(timer) {
        clearTimeout(timer)
    }
    // 设立新定时器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

// 用debounce来包装scroll的回调
const better_scroll = debounce(() => console.log('触发了滚动事件'), 1000)

document.addEventListener('scroll', better_scroll)

使用throttle优化debounce

// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {
  // last为上一次触发回调的时间, timer是定时器
  let last = 0, timer = null
  // 将throttle处理结果当作函数返回
  
  return function () { 
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments
    // 记录本次触发回调的时间
    let now = +new Date()
    
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last < delay) {
    // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
       clearTimeout(timer)
       timer = setTimeout(function () {
          last = now
          fn.apply(context, args)
        }, delay)
    } else {
        // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
        last = now
        fn.apply(context, args)
    }
  }
}

// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)

document.addEventListener('scroll', better_scroll)