在数字时代,速度已成为互联网体验的关键。用户对网页加载时间的容忍度越来越低,每一毫秒的延迟都可能导致用户的流失。根据谷歌的研究,页面加载时间超过3秒的网站,其跳出率会增加120%。在这个以用户为中心的网络世界里,性能优化不再是一个选项,而是必须。
相信大家都至少听过一道面试题从用户输入网址到界面完成展示中间发生了什么。不知道大伙有没有思考过面试官为何要问这个问题,想要考察的内容是什么,单纯是考考你的记忆力吗?我个人认为这道题是面试中的神题,不但能考察基础的技术能力,还能从这道题中继续延伸无数个方向,例如网络安全、浏览器工作原理,也例如本文的主角:性能优化。
DNS 相关优化
用户输入网址后,第一步是解析网址,浏览器需要将这个域名转换为机器可读的IP地址,这是通过DNS(domainnamesystem)查询来完成。
1.DNS 缓存
通常我们的操作系统和浏览器已经自动做了相关缓存
-
浏览器缓存:现代浏览器会缓存DNS查询结果,减少同一域名的重复查询。
-
操作系统缓存:操作系统也会缓存DNS查询结果,通常可以通过命令查看和管理,如ipconfig/displaydns(Windows)或sudodscacheutil-cachedump-entrieshost(macOS)。
-
DNS解析器缓存:本地网络的DNS解析器通常也会缓存查询结果,提高局域网内的查询速度。
2.减少外部资源引用和开启DNS域名预解析
-
条件允许的情况下,合并CSS和JS,减少外部文件请求次数
-
尽可能使用同一个域名下的资源,避免跨域请求
-
对于跨域资源,可以通过dns-prefetch来开启DNS预解析。
<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />
3.启动CDN加速
CDN通过在全球分布的服务器缓存内容,提供更快速的访问。CDN服务提供商通常也提供优化的DNS服务,将用户的DNS查询定向到离用户最近的服务器节点,进一步减少延迟。
HTTP相关优化
解析完域名之后,自然就是开始HTTP连接。在这个阶段,我们能做的事情变得更多了。
1.关于http协议
http协议总是在不断的升级优化中,更高版本的http协议总能带来更好的性能,根据你项目与用户的实际情况,尽可能的升级到合理的版本。这里不赘述http1/2/3之间的区别,想要了解的出门左转问问GPT。
1.1 http1.1的优化
如果你评估了你的项目确实无法升级到http2或更高,那么可以考虑以下优化
-
减少请求次数:举个例子,webpack中有个SplitChunksPlugin的插件,用于分割代码生成产物的,我们可以利用这个插件控制我们的生成物,例如像Vue、Vue-router、Vuex、以及我们用到的一些基础依赖都打包成一个js,对于仅在某些页面引用的依赖进行单独拆分便于按需加载。其他的还有使用雪碧图、将小图片转为base64内嵌到js或html里等手段。
-
持久连接(Keep-Alive):确保服务器和客户端都支持持久连接,允许在同一个TCP连接上进行多次请求和响应。
-
设置缓存头:合理设置HTTP缓存头(如Cache-Control、Expires、ETag),利用浏览器缓存减少对相同资源的重复请求。(这不限于http1.1)
-
在HTTP/1.x中,大多数浏览器对单个域名最多只能同时打开6-8个TCP连接。为了提高并发加载资源的能力,可以将资源分布到多个子域名上,每个子域名可以独立建立连接,从而增加总的并发连接数。
1.2 http2的优化
-
在HTTP/2中,通过二进制分帧和多路复用技术,可以在同一个TCP连接上并行传输多个请求和响应。所以与http1.1不太一样的,我们应该对js、css进行更加细致的切割,方便我们更加精细的控制按需加载,减少需要加载的文件大小,提高首屏速度。
-
服务器推送:利用HTTP/2的服务器推送(ServerPush)功能,服务器可以主动向客户端推送资源,减少客户端请求的次数。例如,在Nginx中配置:
http {
server {
location / {
http2_push_preload on;
add_header Link "<https://example.com/style.css>; rel=preload; as=style";
}
}
}
-
由于域名分片是有一定代价的(dns查询,tcp链接建立、缓存策略的效率等),http2支持多路复用,已经无需域名分片了,如果有使用域名分片,在升级到http2以后尽可能的将资源移至同一域名下。
2.请求包大小优化
显而易见,减少我们请求的资源的总大小是一个非常直观的优化指标。
2.1 启用传输压缩算法
尽管nginx等服务端程序提供了像gzip、brotli等压缩功能,但是这会带来不小的服务端开销,我们可以提前在代码打包时进行压缩。
-
webpack需要安装CompressionWebpackPlugin这个插件
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
// 其他配置项...
plugins: [
// Gzip 压缩
new CompressionPlugin({
algorithm: 'gzip',
filename: '[path][base].gz',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8
}),
// Brotli 压缩
new CompressionPlugin({
algorithm: 'brotliCompress',
filename: '[path][base].br',
test: /\.(js|css|html|svg)$/,
compressionOptions: { level: 11 },
threshold: 10240,
minRatio: 0.8
})
],
};
-
vite需要安装vite-plugin-compression这个插件
import { defineConfig } from 'vite';
import compression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
// Gzip 压缩
compression({
algorithm: 'gzip',
ext: '.gz', // 输出的文件名后缀
threshold: 10240, // 只处理比这个字节大的文件
deleteOriginFile: false // 是否删除源文件
}),
// Brotli 压缩
compression({
algorithm: 'brotliCompress',
ext: '.br', // 输出的文件名后缀
threshold: 10240, // 只处理比这个字节大的文件
deleteOriginFile: false // 是否删除源文件
})
]
});
2.2 按需加载
-
代码分割(CodeSplitting):webpack和vite都是使用import()动态导入语法,轻松实现代码分割,实现按需加载。如果需要更加精确的控制代码分割,还可以使用SplitChunksPlugin(webpack)或output.manualChunks(rollup/vite)
-
单页应用的路由懒加载
react:
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
vue:
const routes = [
{
path: '/about',
component: () => import('./components/About.vue')
}
];
-
图片懒加载:
-
原生懒加载:使用loading="lazy"属性可以原生支持图片和iframe的懒加载
<img src="large-image.jpg" loading="lazy" alt="Lazy Loaded Image">
-
IntersectionObserverAPI:用于懒加载图片、视频、iframe等资源。
const imgObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
imgObserver.observe(img);
});
-
按需加载第三方库:例如lodash替换为lodash-es,像element-ui等UI库使用按需导入组件的方式去使用。
3.HTTP缓存
3.1 强缓存
-
Expires:定义资源过期的具体时间点。浏览器会在该时间点之前使用缓存的资源,而不访问服务器。缺点是由于使用的是具体的时间戳,可能会受到客户端时间不准确的影响
Expires: Wed, 21 Oct 2024 07:28:00 GMT
-
Cache-Control:优先级高于Expires头。它可以通过多种指令控制缓存行为,包括设置缓存的最大有效期(max-age)、指定资源是公共还是私有缓存(public/private)、禁止缓存(no-cache)、以及禁止存储(no-store)等。
Cache-Control: max-age=31536000
3.2 协商缓存
-
Last-Modified和If-Modified-Since:首次访问时返回的请求头Last-Modified代表资源最后修改时间,后续请求会作为If-Modified-Since的值发送到服务端,服务端会通过判断这个值与资源最后修改时间是否一致,如果一致则返回304NOT_MODIFY,不传输资源内容,否则返回200并传输资源内容。
-
ETag和If-None-Match:etag是服务端生成的唯一标识符(通常是资源的哈希值),后续请求中作为If-None-Match的值发送到服务端,服务端判断资源是否修改。由于Last-Modified基于时间并且精度为秒,所以可能没那么精确;etag更加精确但是由于是实时计算资源的哈希值,所以服务端压力更大。
3.3 强缓存与协商缓存的配合使用
上述两种缓存方式并不是互斥选项,可以配合使用。例如配置一定时间的强缓存,强缓存失效后进行协商缓存查看资源是否修改,如无修改可以继续使用缓存资源。
Cache-Control: max-age=31536000
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
// 后续请求
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
3.4 serviceworker
ServiceWorker是一种在浏览器后台运行的脚本,可以拦截和处理网络请求,从而实现缓存和离线访问等功能。通过ServiceWorker,你可以控制资源的缓存策略,包括静态资产和动态请求。
4.浏览器缓存
4.1 cookies
Cookies是一种在客户端(浏览器)存储小量(一般是4KB)数据的机制,用于在多个请求和页面访问之间维持状态信息。它们在Web开发中被广泛使用,可以存储用户的会话信息、用户偏好设置、跟踪用户行为等。
4.2 web storage
是浏览器提供的两组apilocalStorage和sessionStorage,用于存储键值对,大小通常为5M~10M。不同点在于
-
localStorage为永久存储,不主动删除不会消失,sessionStorage会随会话销毁而消失
-
localStorage可以跨窗口使用,sessionStorage不行
4.3 IndexedDB
IndexedDB是一种浏览器内建的NoSQL数据库,允许在客户端存储大量结构化的数据。与WebStorageAPI(localStorage和sessionStorage)相比,IndexedDB提供了更多的功能和灵活性,适合存储复杂数据和执行更复杂的查询操作。
-
存储容量更大,根据不同浏览器的实现,通常有几百M到上G
-
支持更多的数据格式例如对象、数组等,不限于字符串
-
支持事务、索引等
-
异步API,不阻塞渲染,通过promise返回
总结
以上就是我所知道的,在网络层面能对我们的项目有所优化的手段,欢迎大家留下你的不同意见或者补充。后续还打算继续写构建层面、渲染层面以及部分特殊场景的优化,欢迎关注
最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】
软件测试面试文档
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。