24个前端性能优化技巧
性能优化是一把双刃剑,有利有弊。好的一面是它可以提升网站性能,而坏的一面是配置复杂,或者需要遵循的规则太多。此外,一些性能优化规则并非适用于所有场景,应谨慎使用。读者应该以批判的眼光看待本文。
本文优化建议的参考文献将在每条建议之后或文章末尾提供。
1.减少HTTP请求
一个完整的 HTTP 请求需要经过 DNS 查找、TCP 握手、浏览器发送 HTTP 请求、服务器接收请求、服务器处理请求并返回响应、浏览器接收响应等流程。我们通过一个具体的例子来理解 HTTP:
这是一个HTTP请求,文件大小为28.4KB。
术语解释:
- 排队:在请求队列中花费的时间。
- Stalled:TCP 连接建立到实际可以传输数据之间的时间差,包括代理协商时间。
- 代理协商:与代理服务器协商所花费的时间。
- DNS 查找:执行 DNS 查找所花费的时间。页面上的每个不同域名都需要进行 DNS 查找。
- 初始连接/连接中:建立连接所花费的时间,包括 TCP 握手/重试和 SSL 协商。
- SSL:完成 SSL 握手所花费的时间。
- 请求已发送:发送网络请求所花费的时间,通常为一毫秒。
- 等待(TFFB):TFFB是从发出页面请求到收到响应数据第一个字节的时间。
- 内容下载:接收响应数据所花费的时间。
从这个例子中我们可以看出,实际数据下载时间仅占13.05 / 204.16 = 6.39%
总时间的 1/3。文件越小,这个比例越小;文件越大,这个比例越高。这就是为什么建议将多个小文件合并成一个大文件,从而减少 HTTP 请求的数量。
如何合并多个文件
有几种技术可以通过合并文件来减少 HTTP 请求的数量:
1. 使用 Webpack 打包 JavaScript 文件
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
这会将入口点中导入的所有 JavaScript 文件合并到一个包中。
2.使用 Sass 等 CSS 预处理器合并 CSS 文件:
/* main.scss */
@import 'reset';
@import 'variables';
@import 'typography';
@import 'layout';
@import 'components';
然后编译为单个 CSS 文件:
sass main.scss:main.css
参考:
2. 使用 HTTP2
相比HTTP1.1,HTTP2有几个优点:
更快的解析
解析 HTTP 1.1 请求时,服务器必须连续读取字节,直到遇到 CRLF 分隔符。解析 HTTP 2 请求并不那么复杂,因为 HTTP 2 是基于帧的协议,每个帧都有一个字段指示其长度。
多路复用
使用HTTP1.1时,如果要同时发出多个请求,则需要建立多个TCP连接,因为一个TCP连接一次只能处理一个HTTP1.1请求。
在 HTTP2 中,多个请求可以共享一个 TCP 连接,这称为多路复用。每个请求和响应都由一个流表示,并带有唯一的流 ID 来标识。
多个请求和响应可以在 TCP 连接内以乱序发送,然后在目的地使用流 ID 重新组装。
报头压缩
HTTP2 提供了头压缩功能。
例如,考虑以下两个请求:
:authority: unpkg.zhimg.com
:method: GET
:path: /za-js-sdk@2.16.0/dist/zap.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
:authority: zz.bdstatic.com
:method: GET
:path: /linksubmit/push.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
从上面的两个请求中,你可以看到很多数据是重复的。如果我们可以存储相同的头部信息,并只发送它们之间的差异部分,那么就可以节省大量带宽并加快请求速度。
HTTP/2 在客户端和服务器端使用“头表”来跟踪和存储以前发送的键值对,对于相同的数据,不再通过每个请求和响应来发送。
下面是一个简化的示例。假设客户端按顺序发送以下标头请求:
Header1:foo
Header2:bar
Header3:bat
当客户端发送请求时,它会根据标头值创建一个表:
指数 | 标头名称 | 价值 |
---|---|---|
62 | 标题1 | foo |
63 | 标题2 | 酒吧 |
64 | 标题3 | 蝙蝠 |
如果服务器收到请求,就会创建相同的表。
客户端发送下一个请求时,如果请求头相同,则可以直接发送如下的请求头块:
62 63 64
服务器将查找之前建立的表格并将这些数字恢复为它们对应的完整标头。
优先事项
HTTP2可以对比较紧急的请求设置更高的优先级,服务器收到这样的请求后可以优先处理。
流量控制
由于TCP连接的带宽(取决于客户端到服务器的网络带宽)是固定的,当有多个并发请求时,如果一个请求占用的流量较多,则另一个请求占用的流量就会较少。流量控制可以精确控制不同流的流量。
服务器推送
HTTP2 中新增的一个强大功能是,服务器可以对单个客户端请求发送多个响应。换句话说,除了响应初始请求之外,服务器还可以向客户端推送其他资源,而无需客户端明确请求。
例如,当浏览器请求一个网站时,服务器除了返回HTML页面之外,还可以根据HTML页面中资源的URL主动推送资源。
很多网站已经开始使用HTTP2,比如知乎:
其中“h2”指的是HTTP2协议,“http/1.1”指的是HTTP1.1协议。
参考:
3. 使用服务器端渲染
客户端渲染:获取HTML文件,根据需要下载JavaScript文件,运行文件,生成DOM,然后渲染。
服务端渲染:服务端返回HTML文件,客户端只需要解析HTML即可。
- 优点:首屏渲染速度更快,SEO 更好。
- 缺点:配置复杂,增加服务器的计算负荷。
下面我以Vue SSR为例,简单描述一下SSR的流程。
客户端渲染流程
- 访问客户端呈现的网站。
- 服务器返回一个包含资源导入语句和的 HTML 文件
<div id="app"></div>
。 - 客户端通过HTTP向服务器请求资源,当必要的资源加载完成后,执行
new Vue()
实例化并渲染页面。
客户端渲染应用程序的示例(Vue):
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Client-side Rendering Example</title>
</head>
<body>
<!-- Initially empty container -->
<div id="app"></div>
<!-- JavaScript bundle that will render the content -->
<script src="/dist/bundle.js"></script>
</body>
</html>
// main.js (compiled into bundle.js)
import Vue from 'vue';
import App from './App.vue';
// Client-side rendering happens here - after JS loads and executes
new Vue({
render: h => h(App)
}).$mount('#app');
// App.vue
<template>
<div>
<h1>{{ title }}</h1>
<p>This content is rendered client-side.</p>
</div>
</template>
<script>
export default {
data() {
return {
title: 'Hello World'
}
},
// In client-side rendering, this lifecycle hook runs in the browser
mounted() {
console.log('Component mounted in browser');
}
}
</script>
服务端渲染流程
- 访问服务器呈现的网站。
- 服务器检查当前路由组件需要哪些资源文件,然后将这些文件的内容填充到HTML文件中。如果有AJAX请求,则执行AJAX请求进行数据预取并填充到HTML文件中,最终返回本HTML页面。
- 当客户端接收到这个HTML页面后,就可以立即开始渲染页面。同时,页面也会加载资源,当必要的资源加载完毕后,它就开始执行
new Vue()
实例化并接管页面。
服务器端渲染应用程序的示例(Vue):
// server.js
const express = require('express');
const server = express();
const { createBundleRenderer } = require('vue-server-renderer');
// Create a renderer based on the server bundle
const renderer = createBundleRenderer('./dist/vue-ssr-server-bundle.json', {
template: require('fs').readFileSync('./index.template.html', 'utf-8'),
clientManifest: require('./dist/vue-ssr-client-manifest.json')
});
// Handle all routes with the same renderer
server.get('*', (req, res) => {
const context = { url: req.url };
// Render our Vue app to a string
renderer.renderToString(context, (err, html) => {
if (err) {
// Handle error
res.status(500).end('Server Error');
return;
}
// Send the rendered HTML to the client
res.end(html);
});
});
server.listen(8080);
<!-- index.template.html -->
<!DOCTYPE html>
<html>
<head>
<title>Server-side Rendering Example</title>
<!-- Resources injected by the server renderer -->
</head>
<body>
<!-- This will be replaced with the app's HTML -->
<!--vue-ssr-outlet-->
</body>
</html>
// entry-server.js
import { createApp } from './app';
export default context => {
return new Promise((resolve, reject) => {
const { app, router } = createApp();
// Set server-side router's location
router.push(context.url);
// Wait until router has resolved possible async components and hooks
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// No matched routes, reject with 404
if (!matchedComponents.length) {
return reject({ code: 404 });
}
// The Promise resolves to the app instance
resolve(app);
}, reject);
});
}
从上面两个过程我们可以看出,区别在于第二步,客户端渲染的网站会直接返回HTML文件,而服务端渲染的网站则会将页面渲染完整后再返回这个HTML文件。
这样做有什么好处?可以加快内容交付速度。
假设您的网站需要加载四个文件(a、b、c、d)才能完全渲染。每个文件的大小为 1 MB。
这么算的话,客户端渲染的网站需要加载4个文件和一个HTML文件才能完成首页渲染,总共4MB(忽略HTML文件大小)。而服务端渲染的网站只需要加载一个完全渲染好的HTML文件就能完成首页渲染,总共就是已经渲染好的HTML文件的大小(通常不会太大,几百KB左右;我的个人博客网站(SSR)加载一个HTML文件就有400KB)。这就是服务端渲染速度更快的原因。
参考:
4. 使用 CDN 托管静态资源
内容分发网络 (CDN) 是一组分布在多个地理位置的 Web 服务器。我们都知道,服务器距离用户越远,延迟就越高。CDN 旨在通过在多个位置部署服务器来解决这个问题,使用户更接近服务器,从而缩短请求时间。
CDN原则
当用户访问没有CDN的网站时,流程如下:
- 浏览器需要将域名解析为IP地址,因此向本地DNS发出请求。
- 本地DNS依次向根服务器、顶级域名服务器、权威服务器发出请求,获取网站服务器的IP地址。
- 本地DNS将IP地址发送回浏览器,浏览器向网站服务器的IP地址发出请求并接收资源。
如果用户访问的是部署了CDN的网站,流程如下:
- 浏览器需要将域名解析为IP地址,因此向本地DNS发出请求。
- 本地DNS依次向根服务器、顶级域名服务器、权威服务器发出请求,获取全局负载均衡(GSLB)系统的IP地址。
- 然后,本地 DNS 向 GSLB 发出请求。GSLB 的主要功能是根据本地 DNS 的 IP 地址确定用户的位置,筛选出距离用户最近的本地负载均衡 (SLB) 系统,并将该 SLB 的 IP 地址返回给本地 DNS。
- 本地DNS将SLB的IP地址返回给浏览器,浏览器再向SLB发起请求。
- SLB根据浏览器请求的资源和地址选择最优的缓存服务器并返回给浏览器。
- 然后浏览器根据SLB返回的地址重定向到缓存服务器。
- 如果缓存服务器有浏览器需要的资源,就把该资源返回给浏览器;如果没有,就向源服务器请求该资源,然后发送给浏览器,并缓存到本地。
注意:为了更加清晰,上面的图表最好用英文标签重新创建。
参考:
5. 将 CSS 放在头部,将 JavaScript 文件放在底部
- CSS 执行会阻止渲染并阻止 JS 执行
- JS 加载和执行会阻塞 HTML 解析并阻止 CSSOM 构建
如果这些 CSS 和 JS 标签放在 HEAD 标签中,加载和解析时间过长,页面就会一片空白。因此,应该将 JS 文件放在页面最底部(不会阻塞 DOM 解析,但会阻塞渲染),以便在加载 JS 文件之前完成 HTML 解析,从而尽早将页面内容呈现给用户。
那么为什么CSS文件还要放在head中呢?
因为先加载 HTML,再加载 CSS 会让用户第一眼看到一个没有样式的“丑陋”页面。为了避免这种情况,CSS 文件应该放在 head 中。
另外,JS文件也可以放在head中,只要script标签有defer属性即可,defer表示异步下载,延迟执行。
以下是最佳放置位置的示例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Optimized Resource Loading</title>
<!-- CSS in the head for faster rendering -->
<link rel="stylesheet" href="styles.css">
<!-- Critical JS that must load early can use defer -->
<script defer src="critical.js"></script>
</head>
<body>
<header>
<h1>My Website</h1>
<!-- Page content here -->
</header>
<main>
<p>Content that users need to see quickly...</p>
</main>
<footer>
<!-- Footer content -->
</footer>
<!-- Non-critical JavaScript at the bottom -->
<script src="app.js"></script>
<script src="analytics.js"></script>
</body>
</html>
这种方法的解释:
-
CSS 内置
<head>
:确保页面渲染时立即设置样式,防止“无样式内容闪烁”(FOUC)。CSS 会阻塞渲染,但这正是我们在本例中想要的。 -
关键 JS 具有
defer
:该defer
属性告诉浏览器:- 解析 HTML 时并行下载脚本
DOMContentLoaded
仅在 HTML 解析完成后但在事件之前执行脚本- 如果有多个延迟脚本,则保持执行顺序
-
关闭前的非关键 JS
</body>
:没有特殊属性的脚本将:- 在下载和执行时阻止 HTML 解析
- 通过将它们放在底部,我们确保所有重要内容都首先被解析和显示
- 即使总加载时间相同,这也能提高感知性能
您还可以用于async
不依赖于 DOM 或其他脚本的脚本:
<script async src="independent.js"></script>
该async
属性会并行下载脚本,并在脚本可用时立即执行,这可能会中断 HTML 解析。仅当脚本不修改 DOM 或依赖其他脚本时才使用此属性。
参考:
6. 使用字体图标(iconfont)代替图像图标
字体图标是将图标制作成字体。使用时,它就像字体一样,可以设置字体大小、颜色等属性,非常方便。而且,字体图标是矢量图形,不会丢失清晰度。另一个优点是生成的文件特别小。
压缩字体文件
使用fontmin-webpack插件压缩字体文件(感谢Frontend Xiaowei提供)。
参考:
7.充分利用缓存,避免重新加载相同的资源
为了避免用户每次访问网站时都需要请求文件,我们可以通过添加 Expires 或 max-age 来控制此行为。Expires 设置一个时间,只要在此时间之前,浏览器就不会请求文件,而是直接使用缓存。Max-age 是一个相对时间,建议使用 max-age 而不是 Expires。
然而,这带来了一个问题:当文件更新时会发生什么?我们如何通知浏览器再次请求该文件?
这可以通过更新页面中引用的资源链接地址来实现,让浏览器主动放弃缓存并加载新的资源。
具体做法是将资源地址的URL修改与文件内容关联起来,也就是说只有文件内容发生变化,对应的URL才会变化,从而实现文件级的精准缓存控制。什么和文件内容关联呢?我们很自然地想到利用摘要算法,推导出文件的摘要信息。摘要信息与文件内容一一对应,为精确到单个文件粒度的缓存控制提供了依据。
以下是如何实现缓存和缓存破坏的方法:
1.服务器端缓存头(以Express.js为例):
// Set cache control headers for static resources
app.use('/static', express.static('public', {
maxAge: '1y', // Cache for 1 year
etag: true, // Use ETag for validation
lastModified: true // Use Last-Modified for validation
}));
// For HTML files that shouldn't be cached as long
app.get('/*.html', (req, res) => {
res.set({
'Cache-Control': 'public, max-age=300', // Cache for 5 minutes
'Expires': new Date(Date.now() + 300000).toUTCString()
});
// Send HTML content
});
2. 在文件名中使用内容哈希(Webpack 配置):
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js', // Uses content hash in filename
path: path.resolve(__dirname, 'dist'),
},
plugins: [
// Extract CSS into separate files with content hash
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
}),
// Generate HTML with correct hashed filenames
new HtmlWebpackPlugin({
template: 'src/index.html'
})
]
};
这将产生如下输出文件:
main.8e0d62a10c151dad4f8e.js
styles.f4e3a77c616562b26ca1.css
当您更改文件的内容时,其哈希值将会改变,从而迫使浏览器下载新文件而不是使用缓存版本。
3. 使用缓存破坏生成的 HTML 示例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Cache Busting Example</title>
<!-- Note the content hash in the filename -->
<link rel="stylesheet" href="/static/styles.f4e3a77c616562b26ca1.css">
</head>
<body>
<div id="app"></div>
<!-- Script with content hash -->
<script src="/static/main.8e0d62a10c151dad4f8e.js"></script>
</body>
</html>
4.版本查询参数(更简单但效率较低的方法):
<link rel="stylesheet" href="styles.css?v=1.2.3">
<script src="app.js?v=1.2.3"></script>
更新文件时,手动更改版本号以强制重新下载。
参考:
8.压缩文件
压缩文件可以减少文件下载时间,提供更好的用户体验。
得益于webpack和node的发展,文件压缩现在非常方便。
在webpack中,可以使用以下插件进行压缩:
- JavaScript:UglifyPlugin
- CSS:MiniCssExtractPlugin
- HTML:HtmlWebpack插件
事实上,使用 gzip 压缩可以达到更好的效果。只需在 HTTP 请求头的 Accept-Encoding 字段中添加 gzip 标识符即可启用此功能。当然,服务器也必须支持此功能。
Gzip 是目前最流行且有效的压缩方式,例如我用 Vue 开发的一个项目,构建后生成的 app.js 文件大小为 1.4MB,经过 gzip 压缩后只有 573KB,体积减少了近 60%。
下面介绍一下在webpack和node中配置gzip的方法。
下载插件
npm install compression-webpack-plugin --save-dev
npm install compression
webpack 配置
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [new CompressionPlugin()],
}
节点配置
const compression = require('compression')
// Use before other middleware
app.use(compression())
9.图像优化
(1). 延迟加载图像
在页面中,不要初始设置图片路径,仅当图片出现在浏览器视口中时才加载。这就是延迟加载。对于包含大量图片的网站,一次性加载所有图片会对用户体验产生重大影响,因此图片延迟加载是必要的。
首先,像这样设置图像,当图像在页面中不可见时,图像将不会加载:
<img data-src="https://avatars0.githubusercontent.com/u/22117876?s=460&u=7bd8f32788df6988833da6bd155c3cfbebc68006&v=4">
当页面可见时,使用 JS 加载图像:
const img = document.querySelector('img')
img.src = img.dataset.src
图片加载过程如下。完整代码请参考参考资料。
参考:
(2)响应式图像
响应式图像的优点是浏览器可以根据屏幕尺寸自动加载适当的图像。
通过实施picture
<picture>
<source srcset="banner_w1000.jpg" media="(min-width: 801px)">
<source srcset="banner_w800.jpg" media="(max-width: 800px)">
<img src="banner_w800.jpg" alt="">
</picture>
通过实施@media
@media (min-width: 769px) {
.bg {
background-image: url(bg1080.jpg);
}
}
@media (max-width: 768px) {
.bg {
background-image: url(bg768.jpg);
}
}
(3)、调整图像大小
例如,如果您有一张 1920 * 1080 尺寸的图片,您会将其以缩略图的形式显示给用户,并且只有当用户将鼠标悬停在图片上时才会显示完整图片。如果用户实际上从未将鼠标悬停在缩略图上,那么下载图片的时间就浪费了。
因此,我们可以用两张图片来优化。最初只加载缩略图,当用户将鼠标悬停在图片上时,再加载大图。另一种方法是延迟加载大图,手动更改大图的 src 值,以便在所有元素加载完成后再下载。
图像尺寸优化的实现示例:
<!-- HTML Structure -->
<div class="image-container">
<img class="thumbnail" src="thumbnail-small.jpg" alt="Small thumbnail">
<img class="full-size" data-src="image-large.jpg" alt="Full-size image">
</div>
/* CSS for the container and images */
.image-container {
position: relative;
width: 200px;
height: 150px;
overflow: hidden;
}
.thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.full-size {
display: none;
position: absolute;
top: 0;
left: 0;
z-index: 2;
max-width: 600px;
max-height: 400px;
}
/* Show full size on hover */
.image-container:hover .full-size {
display: block;
}
// JavaScript to lazy load the full-size image
document.addEventListener('DOMContentLoaded', () => {
const containers = document.querySelectorAll('.image-container');
containers.forEach(container => {
const thumbnail = container.querySelector('.thumbnail');
const fullSize = container.querySelector('.full-size');
// Load the full-size image when the user hovers over the thumbnail
container.addEventListener('mouseenter', () => {
if (!fullSize.src && fullSize.dataset.src) {
fullSize.src = fullSize.dataset.src;
}
});
// Alternative: Load the full-size image after the page loads completely
/*
window.addEventListener('load', () => {
setTimeout(() => {
if (!fullSize.src && fullSize.dataset.src) {
fullSize.src = fullSize.dataset.src;
}
}, 1000); // Delay loading by 1 second after window load
});
*/
});
});
此实现:
- 最初仅显示缩略图
- 仅当用户将鼠标悬停在缩略图上时才加载全尺寸图像
- 提供一种替代方法,在页面加载后延迟加载所有全尺寸图像
(4)降低图像质量
比如JPG格式的图片,100%质量和90%质量通常看不出来区别,尤其是用作背景图的时候。我在PS里裁剪背景图的时候,经常会把图片裁剪成JPG格式,再压缩到60%质量,基本看不出什么区别。
压缩方式有两种:一种是通过webpack插件image-webpack-loader
,一种是通过在线压缩网站。
以下是如何使用 webpack 插件image-webpack-loader
:
npm i -D image-webpack-loader
webpack 配置
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000, /* Images smaller than 1000 bytes will be automatically converted to base64 code references */
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
/* Compress images */
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
(5)尽可能使用 CSS3 效果代替图像
许多图像可以用 CSS 效果(渐变、阴影等)绘制,在这些情况下,CSS3 效果效果更好。这是因为代码大小通常只是图像大小的一小部分甚至十分之一。
参考:
(6). 使用webp格式图片
WebP 的优势体现在其更优的图像数据压缩算法,在保持肉眼难以辨别的图像质量的同时,带来了更小的图像体积。此外,它还具备无损和有损压缩模式、Alpha 透明度以及动画功能。其对 JPEG 和 PNG 的转换效果也相当出色、稳定且统一。
使用 fallback 实现 WebP 的示例:
<!-- Using the picture element for WebP with fallback -->
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="Description of the image">
</picture>
服务器端 WebP 检测和服务:
// Express.js example
app.get('/images/:imageName', (req, res) => {
const supportsWebP = req.headers.accept && req.headers.accept.includes('image/webp');
const imagePath = supportsWebP
? `public/images/${req.params.imageName}.webp`
: `public/images/${req.params.imageName}.jpg`;
res.sendFile(path.resolve(__dirname, imagePath));
});
参考:
10. 通过 Webpack 按需加载代码,提取第三方库,减少 ES6 转 ES5 时的冗余代码
以下来自官方 Webpack 文档的引文解释了延迟加载的概念:
延迟加载或按需加载是优化网站或应用程序的好方法。这种方法实际上是将代码在某些逻辑断点处分离,然后在某些代码块中完成某些操作后立即引用或即将引用一些新的代码块。这加快了应用程序的初始加载速度,并减轻了其整体体积,因为某些代码块可能永远不会加载。
引用来源:Lazy Loading
注意:图片延迟加载(详见第 9.1 节)会延迟图片资源的加载,直到它们在视口中可见为止。而代码延迟加载则会拆分 JavaScript 包,并仅在特定功能需要时才加载代码片段。这两种方法都能缩短初始加载时间,但它们在资源优化方面的作用程度不同。
根据文件内容生成文件名,结合Import组件动态导入实现按需加载
这个需求可以通过配置 output 的 filename 属性来实现。filename 属性中有一个值选项是 [contenthash],它会根据文件内容创建一个唯一的哈希值。当文件内容发生变化时,[contenthash] 也会随之变化。
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
path: path.resolve(__dirname, '../dist'),
},
Vue 应用程序中代码延迟加载的示例:
// Instead of importing synchronously like this:
// import UserProfile from './components/UserProfile.vue'
// Use dynamic import for route components:
const UserProfile = () => import('./components/UserProfile.vue')
// Then use it in your routes
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: UserProfile }
]
})
这确保了 UserProfile 组件仅在用户导航到该路线时加载,而不是在初始页面加载时加载。
提取第三方库
由于导入的第三方库通常比较稳定且不会频繁更改,因此将它们单独提取作为长期缓存是更好的选择。
这需要使用 webpack4 的 splitChunk 插件的 cacheGroups 选项。
optimization: {
runtimeChunk: {
name: 'manifest' // Split webpack's runtime code into a separate chunk.
},
splitChunks: {
cacheGroups: {
vendor: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
},
}
},
- test:用于控制此缓存组匹配哪些模块。如果不加任何参数,则默认选择所有模块。可传递的值类型:RegExp、String 和 Function;
- priority:表示提取权重,数字越大优先级越高。由于一个模块可能同时满足多个 cacheGroup 的条件,因此提取时会以权重最高的为准;
- reuseExistingChunk:指示是否使用现有块。如果为 true,则表示如果当前块包含已提取的模块,则不会生成新的模块。
- minChunks(默认为 1):拆分前此代码块应被引用的最小次数(注意:为确保代码块可重用性,默认策略不要求拆分多个引用)
- 块(默认为异步):初始、异步和全部
- name(打包的chunk名称):字符串或者函数(函数可以根据条件自定义名称)
将 ES6 转换为 ES5 时减少冗余代码
为了在 Babel 转换后实现与原始代码相同的功能,需要一些辅助函数,例如:
class Person {}
将转换为:
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Person = function Person() {
_classCallCheck(this, Person);
};
这里,_classCallCheck
是一个helper
函数。如果在多个文件中声明了类,那么helper
就会生成多个这样的函数。
包@babel/runtime
中声明了所有需要的辅助函数,的作用是从中@babel/plugin-transform-runtime
导入所有需要函数的文件:helper
@babel/runtime package
"use strict";
var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck");
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
var Person = function Person() {
(0, _classCallCheck3.default)(this, Person);
};
这里,helper
函数classCallCheck
不再被编译,而是helpers/classCallCheck
从中引用@babel/runtime
。
安装
npm i -D @babel/plugin-transform-runtime @babel/runtime
文件
中的使用.babelrc
"plugins": [
"@babel/plugin-transform-runtime"
]
参考:
11.减少回流和重绘
浏览器渲染过程
- 解析 HTML 以生成 DOM 树。
- 解析 CSS 生成 CSSOM 规则树。
- 结合DOM树和CSSOM规则树生成渲染树。
- 遍历渲染树开始布局,计算每个节点的位置和大小信息。
- 将渲染树的每个节点绘制到屏幕上。
回流焊
当DOM元素的位置或者大小发生改变时,浏览器需要重新生成渲染树,这个过程称为重排。
重绘
重新生成渲染树后,渲染树的每个节点都需要绘制到屏幕上,这个过程称为重绘。并非所有操作都会引起重排,例如,更改字体颜色就只会引起重绘。记住,重排会导致重绘,但重绘不会引起重排。
重排和重绘操作都非常昂贵,因为 JavaScript 引擎线程和 GUI 渲染线程是互斥的,并且一次只能工作一个。
哪些操作会引起回流?
- 添加或删除可见的 DOM 元素
- 元素位置改变
- 元素大小变化
- 内容变更
- 浏览器窗口大小改变
如何减少回流和重绘?
- 使用JavaScript修改样式的时候,最好不要直接写样式,而是通过替换类来改变样式。
- 如果需要对某个 DOM 元素执行一系列操作,可以将该 DOM 元素从文档流中取出,进行修改,然后再将其放回文档中。建议使用隐藏元素(display:none)或文档片段(DocumentFragement),它们都可以很好地实现此方法。
造成不必要重流(低效)的示例:
// This causes multiple reflows as each style change triggers a reflow
const element = document.getElementById('myElement');
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';
element.style.padding = '20px';
element.style.borderRadius = '5px';
优化版本 1 - 使用 CSS 类:
/* style.css */
.my-modified-element {
width: 100px;
height: 200px;
margin: 10px;
padding: 20px;
border-radius: 5px;
}
// Only one reflow happens when the class is added
document.getElementById('myElement').classList.add('my-modified-element');
优化版本 2 - 批处理样式更改:
// Batching style changes using cssText
const element = document.getElementById('myElement');
element.style.cssText = 'width: 100px; height: 200px; margin: 10px; padding: 20px; border-radius: 5px;';
优化版本 3 - 使用文档片段(针对多个元素):
// Instead of adding elements one by one
const list = document.getElementById('myList');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
// Only one reflow happens when the fragment is appended
list.appendChild(fragment);
优化版本 4 - 将元素从流中取出,修改,然后重新插入:
// Remove from DOM, make changes, then reinsert
const element = document.getElementById('myElement');
const parent = element.parentNode;
const nextSibling = element.nextSibling;
// Remove (causes one reflow)
parent.removeChild(element);
// Make multiple changes (no reflows while detached)
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';
element.style.padding = '20px';
element.style.borderRadius = '5px';
// Reinsert (causes one more reflow)
if (nextSibling) {
parent.insertBefore(element, nextSibling);
} else {
parent.appendChild(element);
}
优化版本 5 - 暂时使用 display:none:
const element = document.getElementById('myElement');
// Hide element (one reflow)
element.style.display = 'none';
// Make multiple changes (no reflows while hidden)
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';
element.style.padding = '20px';
element.style.borderRadius = '5px';
// Show element again (one more reflow)
element.style.display = 'block';
通过使用这些优化技术,您可以显著减少重流和重绘的次数,从而实现更流畅的性能,尤其是对于动画和动态内容更新。
12. 使用事件委托
事件委托利用事件冒泡的优势,允许您指定单个事件处理程序来管理特定类型的所有事件。所有使用按钮的事件(大多数鼠标事件和键盘事件)都适用于事件委托技术。使用事件委托可以节省内存。
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Pineapple</li>
</ul>
// good
document.querySelector('ul').onclick = (event) => {
const target = event.target
if (target.nodeName === 'LI') {
console.log(target.innerHTML)
}
}
// bad
document.querySelectorAll('li').forEach((e) => {
e.onclick = function() {
console.log(this.innerHTML)
}
})
13. 注意程序局部性
编写良好的计算机程序通常具有良好的局部性;它倾向于引用最近引用的数据项附近的数据项,或引用最近引用的数据项本身。这种倾向被称为局部性原则。具有良好局部性的程序比局部性较差的程序运行速度更快。
局部性通常有两种不同的形式:
- 时间局部性:在具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不久的将来被引用多次。
- 空间局部性:在具有良好空间局部性的程序中,如果某个内存位置已经被引用过一次,则该程序很可能在不久的将来引用附近的内存位置。
时间局部性示例
function sum(arry) {
let i, sum = 0
let len = arry.length
for (i = 0; i < len; i++) {
sum += arry[i]
}
return sum
}
在这个例子中,变量 sum 在每次循环迭代中被引用一次,因此它具有良好的时间局部性。
空间局部性示例
具有良好空间局部性的程序
// Two-dimensional array
function sum1(arry, rows, cols) {
let i, j, sum = 0
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++) {
sum += arry[i][j]
}
}
return sum
}
空间局部性较差的程序
// Two-dimensional array
function sum2(arry, rows, cols) {
let i, j, sum = 0
for (j = 0; j < cols; j++) {
for (i = 0; i < rows; i++) {
sum += arry[i][j]
}
}
return sum
}
回顾上面两个空间局部性的例子,如例子中所示,从每一行开始顺序访问数组中每个元素的方法被称为步幅为 1 的参考模式。
如果数组中每 k 个元素都被访问一次,则被称为步幅为 k 的参考模式。
通常,随着步幅的增加,空间局部性会降低。
这两个例子有什么区别?区别在于,第一个例子按行扫描数组,扫描完一行后再移至下一行;第二个例子按列扫描数组,扫描一行中的一个元素后,立即扫描下一行同一列的元素。
数组按行顺序存储在内存中,因此逐行扫描数组的示例获得了步幅为 1 的参考模式,具有良好的空间局部性;而另一个示例的步幅为行,空间局部性极差。
性能测试
运行环境:
- 中央处理器:i5-7400
- 浏览器:Chrome 70.0.3538.110
对长度为9000的二维数组(子数组长度也是9000)测试空间局部性10次,取平均时间(毫秒),结果如下:
所用的例子是上面提到的两个空间局部性例子。
步幅 1 | 迈莱德 9000 |
---|---|
124 | 2316 |
从上面的测试结果来看,步幅为1的数组的执行速度比步幅为9000的数组快一个数量级。
结论:
- 重复引用相同变量的程序具有良好的时间局部性
- 对于参考模式步幅为k的程序,步幅越小,空间局部性越好;而步幅较大在内存中跳动的程序,空间局部性会很差
参考:
14. if-else 与 switch
随着判断条件的增多,使用switch代替if-else变得越来越可取。
if (color == 'blue') {
} else if (color == 'yellow') {
} else if (color == 'white') {
} else if (color == 'black') {
} else if (color == 'green') {
} else if (color == 'orange') {
} else if (color == 'pink') {
}
switch (color) {
case 'blue':
break
case 'yellow':
break
case 'white':
break
case 'black':
break
case 'green':
break
case 'orange':
break
case 'pink':
break
}
像上面这种情况,从可读性角度来说,使用 switch 更好一些(JavaScript 的 switch 语句不是基于 hash 实现的,而是基于循环判断的,所以从性能角度来说,if-else 和 switch 是一样的)。
为什么 switch 对于多种情况更有利:
-
提升可读性:Switch 语句在处理同一变量的多个条件时,呈现更清晰的视觉结构。case 语句则创建了更有条理的表格格式,更易于浏览和理解。
-
更清晰的代码维护:在 switch 语句中添加或删除条件更简单,错误更少。使用 if-else 链时,很容易意外中断链或忘记“else”关键字。
-
减少重复:在 if-else 示例中,我们重复检查同一个变量(
color
)多次,而在 switch 中我们在顶部指定一次。 -
更适合调试:调试时,在 switch 语句中的特定情况上设置断点比尝试确定需要针对长 if-else 链的哪个部分更容易。
-
意图信号:使用 switch 向其他开发人员传达您正在检查同一变量的多个可能值,而不是可能不相关的条件。
对于现代 JavaScript,还有另一种值得考虑的简单值映射替代方案 - 对象文字:
const colorActions = {
'blue': () => { /* blue action */ },
'yellow': () => { /* yellow action */ },
'white': () => { /* white action */ },
'black': () => { /* black action */ },
'green': () => { /* green action */ },
'orange': () => { /* orange action */ },
'pink': () => { /* pink action */ }
};
// Execute the action if it exists
if (colorActions[color]) {
colorActions[color]();
}
与 if-else 和 switch 语句方法相比,这种方法提供了更好的性能(O(1)查找时间)。
15.查找表
当条件语句较多时,使用 switch 和 if-else 并非最佳选择。在这种情况下,你或许可以尝试查找表。查找表可以使用数组和对象构建。
switch (index) {
case '0':
return result0
case '1':
return result1
case '2':
return result2
case '3':
return result3
case '4':
return result4
case '5':
return result5
case '6':
return result6
case '7':
return result7
case '8':
return result8
case '9':
return result9
case '10':
return result10
case '11':
return result11
}
此 switch 语句可以转换为查找表
const results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11]
return results[index]
如果条件语句不是数值而是字符串,则可以使用对象来构建查找表
const map = {
red: result0,
green: result1,
}
return map[color]
为什么查找表在许多情况下更适合:
-
恒定时间复杂度 (O(1)):查找表可根据索引/键直接访问结果,因此无论选项数量多少,操作时间都是恒定的。相比之下,if-else 链和 switch 语句都具有线性时间复杂度 (O(n)),因为在最坏的情况下,它们可能需要检查所有条件。
-
条件较多时性能提升:随着条件数量的增加,查找表的性能优势更加显著。对于少量条件(2-5 个)的情况,差异可以忽略不计,但当条件数量达到数十或数百个时,查找表的速度会显著加快。
-
代码简洁性:如示例所示,查找表通常需要更少的代码,从而使您的代码库更易于维护。
-
动态配置:查找表可以轻松动态填充:
const actionMap = {};
// Dynamically populate the map
function registerAction(key, handler) {
actionMap[key] = handler;
}
// Register different handlers
registerAction('save', saveDocument);
registerAction('delete', deleteDocument);
// Use it
if (actionMap[userAction]) {
actionMap[userAction]();
}
- 减少认知负荷:当存在许多条件时,查找表可以消除遵循长逻辑链的脑力负担。
何时使用每种方法:
- If-else:最适合一些逻辑复杂或需要检查不同变量的条件(2-3)
- Switch:适用于检查同一变量的中等数量的条件(4-10)
- 查找表:适用于多种条件(10+)或需要 O(1) 访问时间的情况
在实际应用中,查找表可能来自数据库或配置文件等外部来源,这使得它们能够灵活地应对映射逻辑可能发生变化而无需修改代码的情况。
16.避免页面卡顿
60fps 和设备刷新率
目前,大多数设备的屏幕刷新率为每秒 60 次。因此,如果页面上有动画或渐变效果,或者用户正在滚动页面,浏览器需要以与设备屏幕刷新率匹配的速率渲染动画或页面。每帧
的预算时间略大于 16 毫秒(1 秒 / 60 = 16.66 毫秒)。但实际上,浏览器需要进行一些日常工作,因此所有工作都需要在 10 毫秒内完成。如果无法满足此预算,帧率就会下降,屏幕上的内容就会出现抖动。这种现象通常称为卡顿,会对用户体验产生负面影响。
引用来源:Google Web Fundamentals - Rendering Performance
假设你使用 JavaScript 修改 DOM,触发样式更改,经历重排和重绘,最终绘制到屏幕上。如果其中任何一个步骤耗时过长,都会导致此帧的渲染时间过长,平均帧率就会下降。假设此帧耗时 50 毫秒,那么帧率就是 1 秒 / 50 毫秒 = 20fps,页面就会出现卡顿。
对于一些长时间运行的JavaScript,我们可以使用计时器来拆分和延迟执行。
for (let i = 0, len = arry.length; i < len; i++) {
process(arry[i])
}
假设上述循环结构由于 process() 的复杂性高或数组元素太多(或两者兼而有之)而花费的时间太长,您可能需要尝试拆分。
const todo = arry.concat()
setTimeout(function() {
process(todo.shift())
if (todo.length) {
setTimeout(arguments.callee, 25)
} else {
callback(arry)
}
}, 25)
如果你有兴趣了解更多,请查看《高性能 JavaScript》第 6 章和《高效前端:Web 高效编程与优化实践》第 3 章。
参考:
17. 使用 requestAnimationFrame 实现视觉变化
从第 16 点可知,大多数设备的屏幕刷新率为 60 次/秒,这意味着每帧的平均时间为 16.66 毫秒。使用 JavaScript 实现动画效果时,最好的情况是代码在每帧开始时开始执行。确保 JavaScript 在帧开始时运行的唯一方法是使用requestAnimationFrame
。
/**
* If run as a requestAnimationFrame callback, this
* will be run at the start of the frame.
*/
function updateScreen(time) {
// Make visual updates here.
}
requestAnimationFrame(updateScreen);
如果您使用setTimeout
或setInterval
实现动画,回调函数将在帧中的某个点运行,可能就在最后,这通常会导致我们错过帧,从而导致卡顿。
参考:
18. 使用 Web Worker
Web Worker 使用其他工作线程独立于主线程运行。它们可以在不干扰用户界面的情况下执行任务。Worker 可以通过向创建它的 JavaScript 代码指定的事件处理程序发送消息来发送消息(反之亦然)。
Web Workers 适合处理与浏览器 UI 无关的纯数据或长时间运行的脚本。
创建一个新的工作线程很简单,只需指定一个脚本 URI 来执行工作线程(main.js):
var myWorker = new Worker('worker.js');
// You can send messages to the worker through the postMessage() method and onmessage event
first.onchange = function() {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}
second.onchange = function() {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}
在worker中,收到消息后,我们可以编写一个事件处理函数代码作为响应(worker.js):
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
postMessage(workerResult);
}
onmessage 处理函数在收到消息后立即执行,消息本身用作事件的 data 属性。这里我们只需将两个数字相乘,然后再次使用 postMessage() 方法将结果发送回主线程。
回到主线程,我们再次使用 onmessage 来响应从 worker 发回的消息:
myWorker.onmessage = function(e) {
result.textContent = e.data;
console.log('Message received from worker');
}
这里我们从message事件中获取数据,并将其设置为result的textContent,这样用户就可以直接看到计算的结果。
请注意,在 Worker 内部,您无法直接操作 DOM 节点,也无法使用 window 对象的默认方法和属性。但是,您可以使用 window 对象下的很多功能,包括数据存储机制,例如 WebSockets、IndexedDB 以及 Firefox OS 特有的 Data Store API。
参考:
19. 使用按位运算
JavaScript 中的数字使用 IEEE-754 标准以 64 位格式存储。但在按位运算中,数字会被转换为 32 位有符号格式。即使经过转换,按位运算也比其他数学运算和布尔运算快得多。
模数
由于偶数的最低位为0,奇数的最低位为1,因此模运算可以用按位运算代替。
if (value % 2) {
// Odd number
} else {
// Even number
}
// Bitwise operation
if (value & 1) {
// Odd number
} else {
// Even number
}
工作原理:按&
位与运算符将第一个操作数的每个位与第二个操作数的相应位进行比较。如果两个位都为 1,则将相应的结果位设置为 1;否则,将其设置为 0。
当我们这样做时value & 1
,我们只检查数字的最后一位:
- 对于偶数(例如,
100
二进制中的 4 =),最后一位为 0:100 & 001 = 000
(0) - 对于奇数(例如,
101
二进制中的 5 =),最后一位为 1:101 & 001 = 001
(1)
地面
~~10.12 // 10
~~10 // 10
~~'1.5' // 1
~~undefined // 0
~~null // 0
工作原理:按位非运算符~
(bitwise NOT) 将操作数的所有位取反。对于数字n
,~n
等于-(n+1)
。当应用两次 ( ~~n
) 时,它实际上会截断数字的小数部分,类似于Math.floor()
正数和Math.ceil()
负数的情况。
流程:
- 首先
~
:将数字转换为 32 位整数,并反转所有位 - 第二步
~
:再次反转所有位,得到原始数字,但删除小数部分
例如:
~10.12 → ~10 → -(10+1) → -11
~(-11) → -(-11+1) → -(-10) → 10
位掩码
const a = 1
const b = 2
const c = 4
const options = a | b | c
通过定义这些选项,您可以使用按位与运算来确定 a/b/c 是否在选项中。
// Is option b in the options?
if (b & options) {
...
}
工作原理:在位掩码中,每个位代表一个布尔标志。这些值通常是 2 的幂,因此每个位恰好对应一个设置。
a = 1
:二进制001
b = 2
:二进制010
c = 4
:二进制100
options = a | b | c
:(|
按位或)将它们组合起来:(001 | 010 | 100 = 111
二进制)= 7(十进制)
当检查标志是否设置时if (b & options)
:
b & options
=010 & 111
=010
= 2(十进制)- 由于它不为零,因此条件计算结果为真
这种技术对于在单个数字中存储和检查多个布尔值非常有效,并且通常用于系统编程、图形编程和权限系统。
20.不要覆盖本机方法
无论你的 JavaScript 代码如何优化,都无法与原生方法匹敌。这是因为原生方法是用低级语言(C/C++)编写的,并被编译成机器码,成为浏览器的一部分。如果有原生方法可用,请尽量使用它们,尤其是在进行数学运算和 DOM 操作时。
示例:字符串替换(原生 vs. 自定义)
一个常见的陷阱是重写原生字符串方法,例如replaceAll()
。以下是一个低效的自定义实现与原生方法的比较,并附有性能基准:
// Inefficient custom global replacement (manual loop)
function customReplaceAll(str, oldSubstr, newSubstr) {
let result = '';
let index = 0;
while (index < str.length) {
if (str.slice(index, index + oldSubstr.length) === oldSubstr) {
result += newSubstr;
index += oldSubstr.length;
} else {
result += str[index];
index++;
}
}
return result;
}
// Efficient native method (browser-optimized)
function nativeReplaceAll(str, oldSubstr, newSubstr) {
return str.replaceAll(oldSubstr, newSubstr);
}
// Test with a large string (100,000 repetitions of "abc ")
const largeString = 'abc '.repeat(100000);
// Benchmark: Custom implementation
console.time('customReplaceAll');
customReplaceAll(largeString, 'abc', 'xyz');
console.timeEnd('customReplaceAll'); // Output: ~5ms (varies by browser)
// Benchmark: Native method
console.time('nativeReplaceAll');
nativeReplaceAll(largeString, 'abc', 'xyz');
console.timeEnd('nativeReplaceAll'); // Output: ~2ms (typically 2-3x faster)
关键要点
- 性能:像这样的本机方法
replaceAll()
在浏览器级别进行了优化,通常优于手写代码(如上面的基准测试所示)。 - 可维护性:本机方法是标准化的,有据可查的,并且比自定义逻辑更不容易出错(例如,处理重叠子字符串等边缘情况)。
- 生态系统兼容性:使用本机方法可确保与依赖 JavaScript 内置行为的库和工具的一致性。
何时使用自定义代码?
虽然原生方法通常更胜一筹,但在极少数情况下,你可能需要自定义逻辑:
- 当本机方法不存在时(例如,旧版浏览器的 polyfilling)。
- 适用于本机 API 未涵盖的高度专业化的边缘情况。
- 当您需要避免在极其注重性能的循环(例如,紧密的数值计算)中出现函数调用开销时。
请记住:浏览器供应商花费了数百万小时来优化原生方法。通过利用它们,您可以免费获得性能提升,并降低重新发明有缺陷解决方案的风险。
21. 降低 CSS 选择器的复杂性
(1).浏览器读取选择器时,遵循从右到左读取的原则。
让我们看一个例子
#block .text p {
color: red;
}
- 找出所有 P 元素。
- 检查结果 1 中找到的元素是否具有类名为“text”的父元素
- 检查结果 2 中找到的元素是否具有 ID 为“block”的父元素
为什么效率低下?这种从右到左的求值过程在复杂文档中可能非常耗时。以选择器#block .text p
为例:
- 浏览器首先查找文档中的所有元素(可能有数百个)
p
- 对于每个段落元素,它必须检查其祖先元素是否具有该类
text
- 对于通过第 2 步的,必须检查其祖先是否有 ID
block
这会造成严重的性能瓶颈,因为:
- 初始选择(
p
)非常广泛 - 每个后续步骤都需要检查 DOM 树中的多个祖先
- 对每个段落元素重复此过程
更有效的替代方案是:
#block p.specific-text {
color: red;
}
这是更有效的,因为:
- 它直接针对具有特定类别的段落,避免检查所有段落
(2).CSS选择器优先级
Inline > ID selector > Class selector > Tag selector
根据以上两条信息,我们可以得出结论。
- 选择器越短越好。
- 尝试使用高优先级的选择器,例如ID和类选择器。
- 避免使用通用选择器 *。
最佳 CSS 选择器的实用建议:
/* ❌ Inefficient: Too deep, starts with a tag selector */
body div.container ul li a.link {
color: blue;
}
/* ✅ Better: Shorter, starts with a class selector */
.container .link {
color: blue;
}
/* ✅ Best: Direct, single class selector */
.nav-link {
color: blue;
}
最后要说的是,根据我找到的资料,CSS选择器没有必要进行优化,因为最慢和最快的选择器之间的性能差异非常小。
参考:
22. 使用 Flexbox 替代早期的布局模型
在早期的 CSS 布局方法中,我们可以使用绝对定位、相对定位或浮动定位元素。现在,我们有了一种新的布局方法flexbox,它比之前的布局方法有一个优势:性能更佳。
下面的截图显示了在 1300 个盒子上使用浮动的布局成本:
然后我们使用 flexbox 重新创建这个例子:
现在,对于相同数量的元素和相同的视觉外观,布局时间要少得多(在本例中为 3.5 毫秒对 14 毫秒)。
但是,flexbox 兼容性仍然是一个问题,并非所有浏览器都支持它,因此请谨慎使用。
浏览器兼容性:
- Chrome 29+
- Firefox 28+
- Internet Explorer 11
- Opera 17+
- Safari 6.1+(以 -webkit- 为前缀)
- Android 4.4+
- iOS 7.1+(以 -webkit- 为前缀)
参考:
23. 使用变换和不透明度属性实现动画
在 CSS 中,变换和不透明度属性的变化不会触发重排和重绘,它们是可以由合成器单独处理的属性。
示例:低效动画 vs. 高效动画
❌ 使用触发重排和重绘的属性的动画效率低下:
/* CSS */
.box-inefficient {
position: absolute;
left: 0;
top: 0;
width: 100px;
height: 100px;
background-color: #3498db;
animation: move-inefficient 2s infinite alternate;
}
@keyframes move-inefficient {
to {
left: 300px;
top: 200px;
width: 150px;
height: 150px;
}
}
该动画不断触发布局重新计算(回流),因为它为位置(left
/ top
)和大小(width
/ height
)属性设置动画。
✅ 使用变换和不透明度实现高效的动画:
/* CSS */
.box-efficient {
position: absolute;
width: 100px;
height: 100px;
background-color: #3498db;
animation: move-efficient 2s infinite alternate;
}
@keyframes move-efficient {
to {
transform: translate(300px, 200px) scale(1.5);
opacity: 0.7;
}
}
为什么这样更好:
transform: translate(300px, 200px)
替换left: 300px; top: 200px
transform: scale(1.5)
替换width: 150px; height: 150px
- 这些变换操作和不透明度变化可以由 GPU 直接处理,而无需触发布局或绘制操作
性能比较:
- 低效版本可能会在低端设备上丢帧,因为每一帧都需要:
- JavaScript → 样式计算 → 布局 → 绘制 → 合成
- 高效版本通常维持 60fps,因为它只需要:
- JavaScript → 样式计算 → 复合
HTML实现:
<div class="box-inefficient">Inefficient</div>
<div class="box-efficient">Efficient</div>
对于复杂的动画,您可以使用 Chrome DevTools 的性能面板来直观地查看差异。与高效的动画相比,低效的动画会显示更多的布局和绘制事件。
参考:
24.合理使用规则,避免过度优化
性能优化主要分为两类:
- 加载时间优化
- 运行时优化
以上 23 条建议中,前 10 条属于加载时优化,后 13 条属于运行时优化。通常情况下,无需全部应用这 23 条性能优化规则。最好根据网站的用户群体进行有针对性的调整,以节省时间和精力。
在解决问题之前,需要先明确问题所在,否则不知从何入手。所以在进行性能优化之前,最好先调研一下网站的加载和运行性能。
检查装载性能
网站的加载性能主要取决于白屏时间和首屏时间。
- 白屏时间:从输入URL到页面开始显示内容的时间。
- 首屏时间:从输入URL到页面完全呈现的时间。
您可以通过在 之前放置以下脚本来获取白屏时间</head>
。
<script>
new Date() - performance.timing.navigationStart
// You can also use domLoading and navigationStart
performance.timing.domLoading - performance.timing.navigationStart
</script>
new Date() - performance.timing.navigationStart
您可以通过在活动中执行来获得首次屏幕时间window.onload
。
检查运行时性能
通过Chrome的开发者工具,我们可以检查网站在运行时的性能。
打开网站,按 F12 键选择“性能”,点击左上角的灰点,灰点变为红色表示已开始录制。此时,您可以模拟用户使用网站,完成后点击“停止”,即可看到网站运行时的性能报告。如果出现红色块状,表示存在掉帧现象;如果为绿色,则表示 FPS 良好。性能的详细使用情况,请使用搜索引擎搜索,因为范围有限。
通过检查加载和运行时性能,我相信你已经对网站的性能有了大致的了解。所以,你现在需要做的就是运用以上 23 条建议来优化你的网站。行动起来!
参考:
其他参考
结论
性能优化是现代 Web 开发中至关重要的环节,它直接影响用户体验、参与度以及最终的业务成果。本文探讨了涵盖 Web 应用程序各个层面的 24 种不同技术,涵盖网络优化、渲染性能和 JavaScript 执行等各个方面。
关键要点
-
从测量开始,而不是优化。正如第 24 点所述,在应用优化技术之前,务必先确定具体的性能瓶颈。Chrome DevTools 性能面板、Lighthouse 和 WebPageTest 等工具可以帮助您准确定位应用程序的瓶颈所在。
-
专注于关键渲染路径。我们的许多技术(将 CSS 放在头部、将 JavaScript 放在底部、减少 HTTP 请求、服务器端渲染)都是为了加快首次有效绘制的时间——即用户看到并可以与您的内容进行交互的时刻。
-
了解浏览器渲染过程。了解浏览器如何解析 HTML、执行 JavaScript 以及如何将像素渲染到屏幕,对于做出明智的优化决策至关重要,尤其是在处理动画和动态内容时。
-
平衡实施成本与性能提升。并非所有优化技术都值得在每个项目中实施。例如,服务器端渲染会增加复杂性,这对于简单的应用程序来说可能不合理;而按位运算仅在特定的高计算量场景下才能带来性能提升。
-
考虑用户的设备和网络状况。如果您为网速较慢或设备性能较弱地区的用户构建应用,那么图像优化、代码拆分和减少 JavaScript 负载等技术就变得更加重要。
实际实施策略
不要试图一次性实施所有 24 种技术,而应考虑采取分阶段的方法:
-
第一步:实施容易取得成效的措施
- 适当的图像优化
- HTTP/2
- 基本缓存
- CSS/JS 放置
-
第二阶段:解决具体测量瓶颈
- 使用性能分析来识别问题区域
- 根据调查结果应用有针对性的优化
-
持续维护:将性能作为开发工作流程的一部分
- 设定绩效预算
- 实施自动化性能测试
- 审查新增功能对性能的影响
通过将性能视为基本功能而不是事后考虑,您将创建不仅外观美观、功能良好,而且还能提供现代用户期望的速度和响应能力的 Web 应用程序。
请记住,Web 性能是一个持续的过程,而非终点。浏览器不断发展,最佳实践不断变化,用户期望也不断提升。本文中的技术提供了坚实的基础,但紧跟 Web 性能趋势才能确保您的应用程序在未来几年保持快速高效。
鏂囩珷鏉ユ簮锛�https://dev.to/woai3c/24-front-end-performance-optimization-tips-4a6c