前端性能优化

164 阅读15分钟

渲染:减少回流重绘、dom操作使用Fragment

应用:防抖、节流

资源:vue路由懒加载、小程序分包、雪碧图、图片懒加载、dns预解析、压缩、缓存

webpack:dllPligin、多进程打包

没写完,以后学完再写。。。

一、启用前端缓存

所谓的前端缓存,其实就是http缓存,通过(强缓存/协商缓存)等方式让计算机直接从缓存中读取静态资源,从而实现节约宽带提高响应速度减少服务器压力等优化。

参考案例:
中高级前端工程师都需要熟悉的技能--前端缓存 - 掘金 (juejin.cn)

一文!彻底弄懂前端缓存_zz_jesse的博客-CSDN博客

二、开启GZIP压缩

这主要针对工程化项目,如react/vue等。

常规情况下前端部署所需要的dist包中会有一些静态文件(如js,css,图片文件)。这些静态文件会在项目初始化后续某个动作下被加载。出于体积大小的不同,加载速度也不一样。有些文件比较大,加载所需时间相对较长,针对文件加载慢的情况。

我们可以采用一些压缩方案,让这些静态文件的体积尽量变小。这样,就可以相对的节约宽带,而因为这些文件的变小,对这些的静态文件的加载的速度也会得到提升,客户端也可以尽快响应给用户一个良好的体验。

gzip有着比zip更优秀的压缩算法,可以有效的减少文件的大小。

每个框架配置gzip的方法都不太一样,没有标准答案。但是大概的流程都是一样的。

  1. 下载compression-webpack-plugin插件
  2. 配置到webpack中
  3. 通知后端开启gzip

完成

参考案例

react中的umi框架开启gzip.com

vue开启gzip压缩.com

三、使用函数节流和函数防抖

节流和防抖是经典的优化方案。

节流和防抖都是将我们大量重复多余的操作进行合并,以达到减少客户端或服务端压力提高运算速度减少http请求等效果。

参考文章:
前端性能优化篇之函数节流和函数防抖和他们的区别 - 掘金 (juejin.cn)

函数节流与函数防抖_函数节流和函数防抖_何故逸的博客-CSDN博客

四、异步加载script文件或将script文件放在最后加载

浏览器在下载和解析script文件的时候会停止html的解析和 CSSOM 的构建。

所以,在以前我们通常喜欢把< script >标签放在html的最后面。

当然,不想将< script >标签放在后面又不想让script的下载和解析影响html的渲染,也有方案。在script标签中加上defer属性即可。

script标签的defer属性可以让script异步加载并在DOM构建完成和CSS渲染完毕之后再执行。

image.png

写文章 - script标签中的async和defer标签到底是干什么的?详解 - 掘金 (juejin.cn)

HTML script defer 属性 | 菜鸟教程 (runoob.com)

五、减少重排和重绘

重排和重绘是浏览器中相对比较耗时的动作。尤其是重排。

重绘不一定会引起重排。重排一定会导致重绘。

浏览器上我们所能看见的元素。当它们的位置发生改变的时候,并不是流动的。而是先被擦除,再重新生成。这就像画画,当画上的某一个单位需要改变位置,我们无法直接把这个单位直接进行移动,只能先将其擦除,然后在指定的位置重新画一个。

元素改变位置,浏览器会先在指定位置上构建该元素的dom(重排)(注意这里没有渲染),然后在对该元素进行渲染(比如background,color)(重绘)。

元素在位置上的改变属于重排,非位置上的改变基本属于重绘(不绝对)。

比如一个原本红色背景的div,如果仅仅改变背景为蓝色的话,那么只会触发重绘,并不会触发重排。(因为位置没有变,只有CSS改变)

重绘触发场景

  • background的改变
  • color的改变
  • visibility:hidden
  • css3的translate
  • color, border-style, border-radius, visibility, text-decoration, background, background-image, background-position,  background-repeat, background-size,outline-color, outline-style, outline-width, box-shadow
  • ...

重排的触发场景

  • 删除或者新增一个节点元素
  • 元素位置的改变,比如float,position,overflow,display等等
  • 元素尺寸的改变,比如margin,padding,height,width等等
  • 初始化构建DOM树的时候
  • 窗口尺寸的变化 也就是resize事件发生的时候
  • 填充内容的改变(内容撑大了某一个节点,内容改变,包含它的节点大小自然跟随调整。)
  • 读取某一个元素的时候,比如offsetLeft,offsetTop,offsetHeight,offsetWidth, clientTop,clientLeft,clientWidth,clientHeight, scrollTop,scrollLeft,scrollWidth,scrollHeight, width,height等等
  • ...

参考文章:

前端性能优化篇之重绘和回流 - 掘金 (juejin.cn)

重排(reflow)和重绘(repaint) - 掘金 (juejin.cn)

六、使用服务端渲染

如果使用服务端渲染(SSR)的话,首先首屏加载速度会有显著的提升(因为SSE只需要加载首页一个页面)。并且对SEO也很友好

当然它也有弊端:页面数据更容易被爬。服务器压力会变大。

nuxt.js和next.js等都是比较流行SSR框架。

nuxt官网:Nuxt.js 中文网 (nuxtjs.cn)

next中文网:Next.js 中文网 (nuxtjs.cn)

参考文章:

服务端渲染SSR及实现原理 - 掘金 (juejin.cn)

【长文慎入】一文吃透 React SSR 服务端渲染和同构原理 - 掘金 (juejin.cn)

五分钟了解 SPA 与 SSR - 简书 (jianshu.com)

七、将png/jpg/gif图片替换为webp格式图片

webp格式的图片比png/jpg有着更优秀的算法。在图片体积上会比jpg/png更小。所以加载的也就更快,耗费的带宽也就越少。占用加载资源的时间也就越短。

webp格式提供有损压缩无损压缩两种方案。

在线png v/s webp 示例👉WebP 示例 (PNG 转 WebP) (isparta.github.io)

你也可以使用Squoosh来进行在线压缩。然后自己比对下大小(免费的)。

也不用过于担心兼容性问题,已经有95%左右的浏览器支持webp格式。而对于不支持的浏览器,我们也可以采用兼容写法(不过ie完全不支持)。

(体积就是带宽,带宽就是金钱)

参考文章

webp格式图片相对于png/jpb格式图片优点 - 掘金 (juejin.cn)

WebP 相对于 PNG、JPG 有什么优势? - 知乎 (zhihu.com)

八、合并请求

为什么要合并请求?

为了减少请求时间,为了减小服务器压力。ajax请求并不是没有成本的。每次请求都需要进行TCP的三次握手四次挥手,解析报文等一系列的过程,这些过程都需要时间去执行。并且,浏览器在同一域名下的请求并发数有限制,同一域名下同一个请求只能并发一个,不同类型请求(比如GET/POST)并发个数基本在4-6个之间。假设当前浏览器的并发请求有6个。那么第7个请求就需要等前6个请求中任意一个完成以后才可以从任务队列中被拉出去执行。所以,合并请求可以在一定程度上减少资源响应时间,给用户带来更好的使用体验。

浏览器请求并发参考图👇 image.png

测验方法👇(打开网络 => 调整为慢速 => 刷新网页)

image.png 合并请求的基本方案

  1. 使用精灵图(合并静态图片资源请求)
  2. 合理合并get请求,在适当的情况下,我们可以将一些可以合并的get请求合并为一个

九、启用事件委托(事件代理)

事件代理 === 事件委托 (一个东西,叫法不同,以下简称事件委托)

什么是事件委托?利用事件冒泡机制将原本应该绑定在子元素上的事件全部交由父元素来完成的行为被称为事件委托。

事件委托可以减少内存消耗和DOM操作

举个例子🌰(出于可读性方面考虑,以下案例用原生js编写)

javascript
 代码解读
复制代码
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
        ...
    </ul>
    
  /*题目:假设我们需要打印li中的值*/
  
  //方案一 (逐个绑定事件,然后点击打印自身内容)
    const doms = document.getElementsByTagName("li");
    for (let i = 0; i < doms.length; ++i) {
       doms[i].addEventListener("click", (e) => { console.log(e.target.innerText) });
    }
  
  //方案二(事件绑定在父元素(ul)上,但由子元素(ul)触发)
    const dom = document.getElementsByTagName("ul")[0];
    dom.addEventListener("click", (e) => { console.log(e.target.innerText) });

注意方案一和方案二的区别,方案一li自身触发事件,自身执行事件。方案二,li自身触发事件,但全由父元素去执行。方案一需要绑定N个(取决于li的数量)事件。方案二不论由多少个li只需要绑定一个事件。绑定的事件越多,在内存中的占比就越大。

事件委托适用场景:列表数据和瀑布流数据等需要大量绑定相同功能的函数的场景。

参考文章:

超详细!!关于事件冒泡/事件捕获,事件代理(事件委托)这里有你所需要知道的一切。 - 掘金 (juejin.cn)

DOM 事件与事件委托(事件代理) - 知乎 (zhihu.com)

十、尽量使用CSS完成动画效果

一些简单的,需要手动绘制的动画,在CSS可以完成的情况下,尽量避免使用JS完成动画

使用CSS完成动画的好处是:

  1. 不占用主线程(js是需要占用的)
  2. 可以利用硬件加速
  3. 在不可见时动画不会持续执行

当然,如果项目本身存在动画库,建议使用动画库。如果动画复杂,无法使用css完成(比如需要绑定函数),那么建议用JS完成动画。

十一、懒加载(虚拟列表/图片)

对于一些不必要立即显示的节点,我们可以采用懒加载技术。在需要使用到的时候,再去加载该文件(组件),以减少不必要的内存占用和页面负载。

常见使用场景:瀑布流,下拉列表。子组件渲染时机(vue/react)

十三、使用骨架屏

对于用户来说,很多用户会在网页端长时间的白屏状态下失去耐心,然后离开页面。所以在数据查询速度慢,或者资源体积大,数量多无法第一时间返回等浏览器无法快速接受并将数据渲染到视图上的情况下,除了可以采用代码压缩,启动缓存等方案外,我们还可以采用骨架屏的方式来给客户挽回一点体验。

如下 动画2.gif

相关文章

前端骨架屏方案小结 - 掘金 (juejin.cn)

十四、将moment.js换成day.js

好处:

  1. day.js的体积比moment.js小。moment.js有70多kb,但是day.js只有2kb。像微信小程序这种对代码包大小有要求的情况下,day.js会是比moment.js更好的选择。很多官方的框架和库都已经将moment.js换成了day.js。
  2. moment已经好几年没更新了。但是day.js仍在持续更新中。

从moment.js迁移到day.js学习成本并不高,因为day.js是moment.js的微缩版,api相似度极高。

dayjs中文网:Day.js中文网 (fenxianglu.cn)
momentjs中文网:Moment.js 中文网 (momentjs.cn)

十五、webpack

1. Webpack DllPlugin

DllPlugin 是 Webpack 提供的一个插件,用于将一些第三方库或模块打包成动态链接库(DLL)。这样可以在构建时分离这些库,从而加速后续的构建过程。DLL 文件会被缓存,从而减少重新构建的时间。

基本原理

  • DLL 构建:首先创建一个 DLL 文件,其中包含一些不常变动的依赖库。
  • 主构建:在主构建中引用 DLL 文件,从而避免重复打包这些库。

配置步骤

  1. 安装 Webpack 和相关插件

    npm install webpack webpack-cli --save-dev
    
  2. 创建 webpack.dll.config.js

    这是用于构建 DLL 的配置文件。它定义了哪些库将被打包成 DLL 文件。

    const path = require('path');
    const webpack = require('webpack');
    
    module.exports = {
      entry: {
        vendor: ['react', 'react-dom'] // 这里可以添加你的库
      },
      output: {
        path: path.join(__dirname, 'dll'),
        filename: '[name].js',
        library: '[name]_library'
      },
      plugins: [
        new webpack.DllPlugin({
          path: path.join(__dirname, 'dll', '[name]-manifest.json'),
          name: '[name]_library'
        })
      ]
    };
    
  3. 创建主配置文件 webpack.config.js

    在主配置文件中,你需要使用 DllReferencePlugin 来引用 DLL 文件。

    const path = require('path');
    const webpack = require('webpack');
    
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
      },
      plugins: [
        new webpack.DllReferencePlugin({
          context: __dirname,
          manifest: require('./dll/vendor-manifest.json')
        })
      ]
    };
    
  4. 运行构建

    首先,构建 DLL 文件:

    npx webpack --config webpack.dll.config.js
    

    然后,构建主应用:

    npx webpack --config webpack.config.js
    

2. 多进程打包

多进程打包是通过并行处理来加速 Webpack 的构建过程。可以使用 thread-loader 或其他插件来实现。

使用 thread-loader

thread-loader 允许你在 Webpack 中使用线程池来处理加载器,从而加速构建。

  1. 安装 thread-loader

    npm install thread-loader --save-dev
    
  2. 配置 thread-loader

    webpack.config.js 中配置 thread-loader

    javascript
    复制代码
    module.exports = {
      module: {
        rules: [
          {
            test: /.js$/,
            use: [
              'thread-loader',
              'babel-loader'
            ]
          }
        ]
      }
    };
    

    你可以根据需要调整 thread-loader 的配置,例如设置线程池大小等。

使用 parallel-webpack

parallel-webpack 是另一个用于并行处理的插件,可以加速 Webpack 的构建过程。

  1. 安装 parallel-webpack

    bash
    复制代码
    npm install parallel-webpack --save-dev
    
  2. 配置 parallel-webpack

    webpack.config.js 中配置:

    javascript
    复制代码
    const ParallelWebpackPlugin = require('parallel-webpack');
    
    module.exports = {
      plugins: [
        new ParallelWebpackPlugin({
          // 配置选项
        })
      ]
    };
    

总结

  • DllPlugin:用于将不常变动的库打包成 DLL 文件,从而加速构建过程。
  • 多进程打包:通过使用 thread-loaderparallel-webpack 等插件来并行处理构建任务,提高构建速度。

十六、vue的路由懒加载

以往我们配置Vue-router是这样的:

javascript
 代码解读
复制代码
import Vue from 'vue'
import VueRouter from 'vue-router'

// 这里引入子模块
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [{
    path: '/',
    name: 'Home',
    component: Home
  }
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router

上面的例子先加载子组件,然后将子组件命名为Home,最后再将Home赋给Vuecomponent。这样就导致子组件的提前加载。 接下来,实现子组件懒加载,则改动如下:

javascript
 代码解读
复制代码
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [{
    path: '/',
    name: 'Home',
    // 将子组件加载语句封装到一个function中,将function赋给component
    component: () => import( /* webpackChunkName: "home" */ '../views/Home.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router

将子组件加载语句封装到一个function中,将function赋给component。这样就可以实现Vue-router懒加载(按需加载)。

是不是非常简单!哈哈!

可能这里有人会疑惑,component可以接收一个function吗? 这确实可以的。不要被以往的观念束缚。component是对象的一个属性,在Javascript中属性的值是什么类型都可以。

简述另外两种实现Vue-router路由懒加载的方式

1. vue异步加载技术:

ini
 代码解读
复制代码
 1:vue-router配置路由,使用vue的异步组件技术,可以实现懒加载,此时一个组件会生成一个js文件。
 2:component: resolve => require(['放入需要加载的路由地址'], resolve)
css
 代码解读
复制代码
  {
      path: '/problem',
      name: 'problem',
      component: resolve => require(['../pages/home/problemList'], resolve)
    }

2.webpack提供的require.ensure()实现懒加载:

ruby
 代码解读
复制代码
 1:vue-router配置路由,使用webpack的require.ensure技术,也可以实现按需加载。
 2:这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
 3require.ensure可实现按需加载资源,包括js,css等。他会给里面require的文件单独打包,不会和主文件打包在一起。
 4:第一个参数是数组,表明第二个参数里需要依赖的模块,这些会提前加载。
 5:第二个是回调函数,在这个回调函数里面require的文件会被单独打包成一个chunk,不会和主文件打包在一起,这样就生成了两个chunk,第一次加载时只加载主文件。
 6:第三个参数是错误回调。
 7:第四个参数是单独打包的chunk的文件名
javascript
 代码解读
复制代码
import Vue from 'vue';
import Router from 'vue-router';
const HelloWorld=resolve=>{
		require.ensure(['@/components/HelloWorld'],()=>{
			resolve(require('@/components/HelloWorld'))
		})
	}
Vue.use('Router')
export default new Router({
	routes:[{
	{path:'./',
	name:'HelloWorld',
	component:HelloWorld
	}
	}]
})

import和require的比较(了解)

1import 是解构过程并且是编译时执行
2require 是赋值过程并且是运行时才执行,也就是异步加载
3require的性能相对于import稍低,因为require是在运行时才引入模块并且还赋值给某个变量

十七、DocumentFragment

使用DocumentFragment能解决直接操作DOM引发大量回流的问题,因为所有的节点会被一次插入到文档中,而这个操作仅发生一个重渲染的操作,比如我们要给ul添加五个li节点,区别就像这样: 直接操作DOM,回流五次:

                let app = document.querySelector('.app')
		for(let i = 0;i<5;i++){
			let div = document.createElement('li')
			div.setAttribute('class','item')
			div.innerText = 6666
			app.appendChild(div)
		}

使用DocumentFragment一次性添加,回流一次:

                let app = document.querySelector('.app')
		let fragement = document.createDocumentFragment()
		for(let i = 0;i<5;i++){
			let div = document.createElement('li')
			div.setAttribute('class','item')
			div.innerText = 6666
			fragement.appendChild(div)
		}
		app.appendChild(fragement)

总结:DocumentFragment节点不属于文档树,存在于内存中,并不在DOM中,所以将子元素插入到文档片段中时不会引起页面回流,因此使用DocumentFragment可以起到性能优化的作用。 

十八、DNS预解析

juejin.cn/post/728591…

十九、CDN缓存

使用 CDN(内容分发网络)对首屏优化可以大幅提高前端性能,减少首屏加载时间。下面是几个关键的优化方法:

1. 静态资源缓存

  • CDN 缓存:CDN 会缓存静态资源文件,如 HTML、CSS、JS、图片、字体等,将它们分发到全球各地的节点,用户可以从最近的 CDN 节点获取这些资源,减少请求延迟。

    • 优化措施

      • 设置适当的 Cache-ControlExpires 响应头,让 CDN 和浏览器缓存这些静态资源。
      • 使用文件指纹(例如 style.css?v=123)来进行版本管理,避免修改静态资源后 CDN 还在使用旧版本。
  • 示例

    bash
    复制代码
    Cache-Control: max-age=31536000, public
    

2. 使用 CDN 加速关键资源

  • 对于首屏渲染所需的关键资源(如核心 JS、CSS 文件),确保它们能通过 CDN 优先加载。

  • 建议使用

    • 核心框架库(如 Vue.js、React.js 等)通过 CDN 加载,可以直接使用 CDN 提供的公共资源库(如 cdnjsjsdelivr 等)。
    • 将所有首屏所需的静态资源尽量分配到不同 CDN 域名下,提高资源并行加载速度。
  • 示例

    html
    复制代码
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/your-css-lib.min.css">
    <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
    

3. 延迟加载非首屏资源

  • 延迟加载非关键 CSS 和 JS:对于非首屏所需的资源可以延迟加载,减少首次加载时的 HTTP 请求和资源消耗。

    • 使用 asyncdefer 关键字延迟加载 JavaScript。
    • 对于图片、视频等使用懒加载技术(lazy loading)。
  • 示例

    html
    复制代码
    <script src="main.js" defer></script>
    
  • 对于 CSS,可以通过异步加载的方式:

    html
    复制代码
    <link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
    

4. 首屏内容内联(Critical CSS & JS 内联)

  • CSS 内联:将首屏需要渲染的关键 CSS 直接写入 HTML 的 <style> 标签中,避免首屏加载时浏览器等待外部 CSS 文件的下载。内联的 CSS 只包含首屏所需的样式,其他部分可以延迟加载。

  • JS 内联:对于渲染首屏所需的少量 JavaScript 也可以直接内联到 HTML 文件中,减少 HTTP 请求。

  • 示例

    html
    复制代码
    <style>
      /* Critical CSS for above-the-fold content */
      body { margin: 0; font-family: Arial, sans-serif; }
      header { background-color: #333; color: white; }
    </style>
    

5. 使用 HTTP/2 和多域名并发

  • HTTP/2:确保你的 CDN 支持 HTTP/2,HTTP/2 可以大大提高资源加载速度,支持多路复用(multiplexing),允许多个资源在同一个连接上并发传输,减少资源加载的等待时间。
  • 多域名并发加载:可以将静态资源分布到多个 CDN 域名中,这样浏览器可以同时发起更多的并行请求来加载资源。

6. CDN 缓存 HTML

  • 如果你的网站内容变化不频繁,可以利用 CDN 缓存 HTML 页面。通过动态内容和静态内容分离的策略,只对首屏或静态内容进行 CDN 缓存,动态内容可以通过 Ajax 等方式延迟加载。
  • 配置 CDN 设置缓存策略,根据页面更新的频率设置合理的缓存过期时间,必要时可以通过 ETag 或 Last-Modified 来控制缓存更新。

7. 压缩与打包资源

  • Gzip/Brotli 压缩:确保你的静态资源(CSS、JS、HTML 等)在通过 CDN 分发前进行 Gzip 或 Brotli 压缩,减少文件传输大小。

  • 打包与合并资源:使用 Webpack 等工具将 JS、CSS 进行打包和合并,减少 HTTP 请求的数量。

  • 示例配置

    • 在 Nginx 配置中启用 Gzip:

      bash
      复制代码
      gzip on;
      gzip_types text/css application/javascript;
      

8. DNS 预解析与预连接

  • 使用 <link> 标签进行 DNS 预解析(DNS Prefetching)预连接(Preconnect) ,可以让浏览器提前解析 DNS 和建立连接,减少网络延迟。

    • DNS 预解析:帮助浏览器提前解析某些域名,避免加载资源时因为 DNS 解析而导致的延迟。
    • 预连接:让浏览器在需要加载资源之前就提前建立 TCP 连接和 TLS 握手。
  • 示例

    html
    复制代码
    <!-- 预解析 DNS -->
    <link rel="dns-prefetch" href="//cdn.example.com">
    <!-- 预先连接 CDN -->
    <link rel="preconnect" href="//cdn.example.com" crossorigin>
    

9. Lazy Loading 懒加载

  • 对于图片、视频等资源,使用 lazy-loading 技术,使得只有当这些资源接近视口时才进行加载,避免阻塞首屏渲染。

  • 示例

    html
    复制代码
    <img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy">
    

10. 使用 CDN 的边缘计算功能(Edge Computing)

  • 部分高级 CDN 提供边缘计算功能,可以在 CDN 的边缘节点上执行某些计算和逻辑,从而减少对后端服务器的依赖。例如,将部分渲染逻辑或数据处理移动到 CDN 节点,提高首屏响应速度。

总结

通过 CDN 对首屏进行优化,主要是通过减少请求延迟减少资源大小提高并行加载能力优化缓存策略来提升页面的加载速度。具体的步骤包括缓存静态资源、延迟加载非首屏内容、优化资源加载顺序等。

链接:juejin.cn/post/732626…