跳到主要内容

面试过程中面试官问到前端性能优化有哪些,当我咔咔一顿输出之后面试官追问:前端可以做的性能优化有哪些呢?

前端优化大概可以有以下几个方向:

  • 网络优化
  • 页面渲染优化
  • JS优化
  • 图片优化
  • webpack打包优化
  • React优化
  • Vue优化

网络优化

DNS 预解析

link 标签的 rel 属性设置 dns-prefetch,提前获取域名对应的 IP 地址

使用缓存

减轻服务端压力,快速得到数据(强缓存和协商缓存可以看这里)

使用 CDN(内容分发网络)

用户与服务器的物理距离对响应时间也有影响。

内容分发网络(CDN)是一组分散在不同地理位置的 web 服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。

压缩响应

压缩组件通过减少 HTTP 请求产生的响应包的大小,从而降低传输时间的方式来提高性能。从 HTTP1.1 开始,Web 客户端可以通过 HTTP 请求中的 Accept-Encoding 头来标识对压缩的支持(这个请求头会列出一系列的压缩方法)

如果 Web 服务器看到请求中的这个头,就会使用客户端列出的方法中的一种来压缩响应。Web 服务器通过响应中的 Content-Encoding 头来告知 Web 客户端使用哪种方法进行的压缩

目前许多网站通常会压缩 HTML 文档,脚本和样式表的压缩也是值得的(包括 XML 和 JSON 在内的任何文本响应理论上都值得被压缩)。但是,图片和 PDF 文件不应该被压缩,因为它们本来已经被压缩了。

使用多个域名

Chrome 等现代化浏览器,都会有同域名限制并发下载数的情况,不同的浏览器及版本都不一样,使用不同的域名可以最大化下载线程,但注意保持在 2~4 个域名内,以避免 DNS 查询损耗。

避免图片 src 为空

虽然 src 属性为空字符串,但浏览器仍然会向服务器发起一个 HTTP 请求:

IE 向页面所在的目录发送请求; Safari、Chrome、Firefox 向页面本身发送请求; Opera 不执行任何操作。

页面渲染优化

Webkit 渲染引擎流程:

  • 处理 HTML 并构建 DOM 树
  • 处理 CSS 构建 CSS 规则树(CSSOM)
  • DOM Tree 和 CSSOM Tree 合成一棵渲染树 Render Tree。
  • 根据渲染树来布局,计算每个节点的位置
  • 调用 GPU 绘制,合成图层,显示在屏幕上

避免 css 阻塞

css 影响 renderTree 的构建,会阻塞页面的渲染,因此应该尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现静态资源加载速度的优化)的将 css 资源加载

降低 css 选择器的复杂度

浏览器读取选择器,遵循的原则是从选择器的右边到左边读取。

  • 减少嵌套:最多不要超过三层,并且后代选择器的开销较高,慎重使用
  • 避免使用通配符,对用到的元素进行匹配即可
  • 利用继承,避免重复匹配和定义
  • 正确使用类选择器和 id 选择器

避免使用 CSS 表达式

css 表达式会被频繁地计算。

避免 js 阻塞

js 可以修改 CSSOM 和 DOM,因此 js 会阻塞页面的解析和渲染,并且会等待 css 资源的加载。也就是说 js 会抢走渲染引擎的控制权。所以我们需要给 js 资源添加 defer 或者 async,延迟 js 脚本的执行。

使用外链式的 js 和 css

在现实环境中使用外部文件通常会产生较快的页面,因为 JavaScript 和 CSS 有机会被浏览器缓存起来。对于内联的情况,由于 HTML 文档通常不会被配置为可以进行缓存的,所以每次请求 HTML 文档都要下载 JavaScript 和 CSS。所以,如果 JavaScript 和 CSS 在外部文件中,浏览器可以缓存它们,HTML 文档的大小会被减少而不必增加 HTTP 请求数量。

使用字体图标 iconfont 代替图片图标

  • 图片会增加网络请求次数,从而拖慢页面加载时间
  • iconfont 可以很好的缩放并且不会添加额外的请求

首屏加载优化

  • 使用骨架屏或者动画优化用户体验
  • 资源按需加载,首页不需要的资源延迟加载

减少重绘和回流

  • 增加多个节点使用 documentFragment:不是真实 dom 的部分,不会引起重绘和回流

  • 用 translate 代替 top ,因为 top 会触发回流,但是 translate 不会。所以 translate 会比 top 节省了一个 layout 的时间

  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局);opacity 代替 visiabilityvisiability会触发重绘(paint),但 opacity 不会。

  • 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改 100 次,然后再把它显示出来

  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量

    for (let i = 0; i < 1000; i++) {
    // 获取 offsetTop 会导致回流,因为需要去获取正确的值
    console.log(document.querySelector(".test").style.offsetTop);
    }
  • 尽量少用 table 布局,table 布局的话,每次有单元格布局改变,都会进行整个 tabel 回流重绘;

  • 最好别频繁去操作 DOM 节点,最好把需要操作的样式,提前写成 class,之后需要修改。只需要修改一次,需要修改的时候,直接修改 className,做成一次性更新多条 css DOM 属性,一次回流重绘总比多次回流重绘要付出的成本低得多;

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

  • 每次访问 DOM 的偏移量属性的时候,例如获取一个元素的 scrollTop、scrollLeft、scrollWidth、offsetTop、offsetLeft、offsetWidth、offsetHeight 之类的属性,浏览器为了保证值的正确也会回流取得最新的值,所以如果你要多次操作,最取完做个缓存。更加不要 for 循环中访问 DOM 偏移量属性,而且使用的时候,最好定义一个变量,把要需要的值赋值进去,进行值缓存,把回流重绘的次数减少;

  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层。

JS 中的性能优化

使用事件委托

防抖和节流

尽量不要使用JS 动画

css3 动画canvas 动画都比 JS 动画性能好

多线程

复杂的计算开启 webWorker 进行计算,避免页面假死

计算结果缓存

减少运算次数,比如 vue 中的 computed

图片的优化

雪碧图

借助减少 http 请求次数来进行优化

图片懒加载

在图片即将进入可视区域的时候进行加载(判断图片进入可视区域请参考这里

使用 CSS3 代替图片

有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好

图片压缩

压缩方法有两种,一是通过在线网站进行压缩,二是通过 webpack 插件 image-webpack-loader。它是基于 imagemin 这个 Node 库来实现图片压缩的。

使用渐进式 jpeg

使用渐进式 jpeg,会提高用户体验 参考文章

使用 webp 格式的图片

webp 是一种新的图片文件格式,它提供了有损压缩和无损压缩两种方式。在相同图片质量下,webp 的体积比 png 和 jpg 更小。

webpack 打包优化

缩小 loader 匹配范围

  • 优化 loader 配置
  • test、include、exclude 三个配置项来缩⼩ loader 的处理范围
  • 推荐 include
include: path.resolve(__dirname, "./src"),

resolve.modules

resolve.modules 用于配置 webpack 去哪些目录下寻找第三方模块,默认是 node_modules。

寻找第三方,默认是在当前项目目录下的 node_modules 里面去找,如果没有找到,就会去上一级目录../node_modules 找,再没有会去../../node_modules 中找,以此类推,和 Node.js 的模块寻找机制很类似。

如果我们的第三⽅模块都安装在了项⽬根⽬录下,就可以直接指明这个路径。

module.exports = {
resolve: {
modules: [path.resolve(__dirname, "./node_modules")],
},
};

resolve.extensions

resolve.extensions 在导⼊语句没带⽂件后缀时,webpack 会⾃动带上后缀后,去尝试查找⽂件是否存在。

  • 后缀尝试列表尽量的⼩
  • 导⼊语句尽量的带上后缀。

如果想优化到极致的话,不建议用 extensionx, 因为它会消耗一些性能。虽然它可以带来一些便利。

抽离 css

借助 mini-css-extract-plugin:本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
{
test: /\.less$/,
use: [
// "style-loader", // 不再需要style-loader,⽤MiniCssExtractPlugin.loader代替
MiniCssExtractPlugin.loader,
"css-loader", // 编译css
"postcss-loader",
"less-loader" // 编译less
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name]_[contenthash:6].css",
chunkFilename: "[id].css"
})
]

代码压缩

JS 代码压缩

mode:production,使用的是 terser-webpack-plugin

module.exports = {
// ...
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({}),
]
}
}

CSS 代码压缩

css-minimizer-webpack-plugin

module.exports = {
// ...
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin({})
]
}
}

Html 文件代码压缩

module.exports = {
...
plugin:[
new HtmlwebpackPlugin({
...
minify:{
minifyCSS:false, // 是否压缩css
collapseWhitespace:false, // 是否折叠空格
removeComments:true // 是否移除注释
}
})
]
}

设置了minify,实际会使用另一个插件html-minifier-terser

文件大小压缩

对文件的大小进行压缩,减少http传输过程中宽带的损耗

npm install compression-webpack-plugin -D

new ComepressionPlugin({
test:/.(css|js)$/, // 哪些文件需要压缩
threshold:500, // 设置文件多大开始压缩
minRatio:0.7, // 至少压缩的比例
algorithm:"gzip", // 采用的压缩算法
})

图片压缩

一般来说在打包之后,一些图片文件的大小是远远要比 js 或者 css 文件要来的大,所以图片压缩较为重要

配置方法如下:

module: {
rules: [
{
test: /.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
}
},
{
loader: 'image-webpack-loader',
options: {
// 压缩 jpeg 的配置
mozjpeg: {
progressive: true,
quality: 65
},
// 使用 imagemin**-optipng 压缩 png,enable: false 为关闭
optipng: {
enabled: false,
},
// 使用 imagemin-pngquant 压缩 png
pngquant: {
quality: '65-90',
speed: 4
},
// 压缩 gif 的配置
gifsicle: {
interlaced: false,
},
// 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
webp: {
quality: 75
}
}
}
]
},
]
}

Tree shaking 去除死代码

Tree Shaking 是一个术语,在计算机中表示消除死代码,依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系)

webpack实现Tree shaking有两种不同的方案:

  • usedExports:通过标记某些函数是否被使用,之后通过 Terser 来进行优化的
  • sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用

两种不同的配置方案, 有不同的效果

usedExports

配置方法也很简单,只需要将usedExports设为true

module.exports = {
...
optimization:{
usedExports
}
}

使用之后,没被用上的代码在webpack打包中会加入unused harmony export mul注释,用来告知 Terser 在优化时,可以删除掉这段代码

sideEffects

sideEffects用于告知webpack compiler哪些模块时有副作用,配置方法是在package.json中设置sideEffects属性

如果sideEffects设置为 false,就是告知webpack可以安全的删除未用到的exports

如果有些文件需要保留,可以设置为数组的形式

"sideEffecis":[
"./src/util/format.js",
"*.css" // 所有的css文件
]

上述都是关于javascripttree shakingcss同样也能够实现tree shaking

css tree shaking

css进行tree shaking优化可以安装PurgeCss插件

npm install purgecss-plugin-webpack -D

const PurgeCssPlugin = require('purgecss-webpack-plugin')
module.exports = {
...
plugins:[
new PurgeCssPlugin({
path:glob.sync(`${path.resolve('./src')}/**/*`), {nodir:true}// src里面的所有文件
satelist:function(){
return {
standard:["html"]
}
}
})
]
}

  • paths:表示要检测哪些目录下的内容需要被分析,配合使用 glob
  • 默认情况下,Purgecss 会将我们的 html 标签的样式移除掉,如果我们希望保留,可以添加一个 safelist 的属性

babel-plugin-transform-runtime减少 ES6 转化 ES5 的冗余

Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数。在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass') 的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小。

代码分离

将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件

默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度

代码分离可以分出更小的bundle,以及控制资源加载优先级,提供代码的加载性能

这里通过splitChunksPlugin来实现,该插件webpack已经默认安装和集成,只需要配置即可

默认配置中,chunks 仅仅针对于异步(async)请求,我们可以设置为 initial 或者 all

module.exports = {
...
optimization: {
splitchunks: {
chunks: "all";
}
}
}

splitChunks主要属性有如下:

  • Chunks,对同步代码还是异步代码进行处理
  • minSize: 拆分包的大小, 至少为 minSize,如何包的大小不超过 minSize,这个包不会拆分
  • maxSize: 将大于 maxSize 的包,拆分为不小于 minSize 的包
  • minChunks:被引入的次数,默认是 1

多线程打包提升打包速度

vue

  1. v-for 添加 key
  2. 路由懒加载
  3. 第三方插件按需引入
  4. 合理使用 computed 和 watch
  5. v-for 的同时避免使用 v-if
  6. destory 时销毁事件:比如 addEventListener 添加的事件、setTimeout、setInterval、bus.$on 绑定的监听事件等

react

  1. map 循环展示添加 key

  2. 路由懒加载

  3. 第三方插件按需引入

  4. 使用 scu,memo 或者 pureComponent 避免不必要的渲染

  5. 合理使用 useMemo、memo、useCallback

    他们三个的应用场景都是缓存结果,当依赖值没有改变时避免不必要的计算或者渲染。

    • useCallback 是针对函数进行“记忆”的,当它依赖项没有发生改变时,那么该函数的引用并不会随着组件的刷新而被重新赋值。当我们觉得一个函数不需要随着组件的更新而更新引用地址的时候,我们就可以使用 useCallback 去修饰它。
    • React.memo 是对组件进行 “记忆”,当它接收的 props 没有发生改变的时候,那么它将返回上次渲染的结果,不会重新执行函数返回新的渲染结果。
    • React.useMemo 是针对 值计算 的一种“记忆“,当依赖项没有发生改变时,那么无需再去计算,直接使用之前的值,对于组件而言,这带来的一个好处就是,可以减少一些计算,避免一些多余的渲染。当我们遇到一些数据需要在组件内部进行计算的时候,可以考虑一下 React.useMemo

参考链接

learnku.com/docs/f2e-pe…

segmentfault.com/a/119000004…

juejin.cn/post/695129…

信息

作者:let_code 链接:https://juejin.cn/post/7194400984490049573 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。