从事前端开发的很难离开性能优化 这个词,但是关于前端性能方面,其实知识点是相当琐碎的,以至于我们总是感觉做的不够好 接下来我们从一道面试题说起:
从输入 URL 到页面加载完成,发生了什么?
这个题的答案,其实很简单,相信很多开发的小伙伴都可以很快速的讲出来。今天我们不去单纯的去聊发生了什么,从性能的层面去考虑。
首先,我们需要通过 DNS(域名解析系统)将 URL 解析为对应的 IP 地址,然后与这个 IP 地址确定的那台服务器建立起 TCP 网络连接,随后我们向服务端抛出我们的 HTTP 请求,服务端处理完我们的请求之后,把目标数据放在 HTTP 响应里返回给客户端,拿到响应数据的浏览器就可以开始走一个渲染的流程。渲染完毕,页面便呈现给了用户。这是一个大致的流程。接下来我们把其中的重点分析一下:
-
DNS解析时间:DNS解析的时间会影响整个页面加载的速度。可以通过减少DNS查询次数、使用DNS缓存等方式来优化DNS解析时间。
-
TCP连接时间:建立TCP连接需要进行三次握手,这个过程会消耗一定的时间。可以通过使用HTTP/2协议、TCP连接复用等方式来减少TCP连接时间。
-
请求响应时间:服务器处理请求和返回响应的时间也会影响页面加载速度。可以通过优化服务器端的代码、减少请求的数量和大小等方式来减少请求响应时间。
-
资源加载时间:资源的加载时间也会影响页面加载速度。可以通过使用CDN加速、压缩资源、合并请求等方式来优化资源加载时间。
-
页面渲染时间:页面的渲染时间也会影响用户的感知。可以通过优化CSS和JavaScript的加载和执行、减少DOM操作等方式来优化页面渲染时间。
当我们从性能方面分析出来之后,我们发现有很多地方都涉及到了性能优化的知识点。首先是
- HTTP协议和TCP连接、Vite构建工具
- 图片的合理使用
- 代码优化(合并请求、减少DOM)
- 服务端加载(SSR)技术、CDN加速
- 浏览器缓存机制
以上都是我们对性能优化所涉及到的知识。
HTTP && Vite方面(资源整合/依赖分析/Gzip压缩/Tree Shaking/生产构建优化)
从输入URL到显示页面这一步骤,我们可以将其分为三个主要阶段:
- DNS解析
- TCP连接
- HTTP请求/响应
在前端开发中,对于DNS解析和TCP连接这两个阶段,我们的干预和改进有限。相比之下,HTTP层面的优化才是网络性能优化的核心。因此,我们可以直截了当地从HTTP优化开始谈起,因为这是我们能够主动改进的方面。
HTTP优化主要围绕两个核心方向展开:
- 减少请求次数:这包括减少需要从服务器请求的资源数量,以降低页面加载时间。
- 减少单个请求所需的时间:这涉及优化每个请求的响应时间,以加快页面渲染速度。
这两个优化方向直接引导我们关注在日常开发中常见的操作,即资源的压缩和合并,以提高网站性能!
对于资源整合,这是我们日常用构建工具经常去做的事情,目前较为流行的构建工具也就是Webpack 和Vite(在当前卷之又卷的时代,构建工具也是层出不穷,这里就不再介绍了)对于Webpack相信很多小伙伴也都非常的熟悉了,这里也不再单独去聊了,今天我们专门来聊一聊Vite的性能优化都有哪些?
快速编译和热模块更换
Vite 使用原生 ES 模块和现代浏览器 API 来动态编译代码,从而在浏览器中提供快速的构建时间和即时更新。这种方法消除了在开发过程中对捆绑程序的需求,这可以显著减少构建和部署应用程序所花费的时间。Vite 内置的开发服务器针对快速重新加载和热模块替换进行了优化,允许开发人员实时查看他们对代码所做的更改,而无需刷新整个页面。
模块的延迟加载
Vite 实现了模块的延迟加载,代码只在实际需要时才加载。这样可以减小捆包尺寸并提高性能,特别是对于较大的应用程序。延迟加载还允许用户更快地初始加载时间,因为应用程序非关键部分的代码仅在需要时加载。
Tree Shaking和代码拆分
Vite 的摇树和代码拆分是有助于减小代码大小和提高性能的优化技术。摇树从应用程序中删除未使用的代码,而代码拆分允许您将代码划分为更小、更易于管理的块,并按需加载。这些功能协同工作,以确保您的用户只下载当前页面所需的代码,从而缩短加载时间并提高整体性能
内置开发服务器
Vite 包括一个内置的开发服务器,针对快速重新加载和热模块更换进行了优化。通过此服务器,可以轻松开发和测试应用程序,并允许您实时查看对代码所做的更改,而无需刷新整个页面。开发服务器还支持自动重新加载代码,因此您可以快速迭代更改,而无需手动重新加载。
上面介绍了一些Vite的一些特点,但是我们怎么在项目中更好的利用这些特点呐?
首先就是资源合并:
js
// vite.config.js
build: {
// argument:boolean | 'terser' | 'esbuild'
minify: 'esbuild',
emptyOutDir: true,
// Do not open when packing
sourcemap: false,
target: 'es2015',
cssTarget: 'chrome80',
rollupOptions: {
onwarn(warning, warn) {
if (warning.code === 'EVAL') return
// Use the default warning handling
warn(warning)
},
treeshake: true,
output: {
manualChunks: {
XXX0: ['vue', 'vuex', 'vue-router', 'dayjs', 'axios', 'vue-i18n'],
XXX1: ['element-plus', '@element-plus/icons-vue'],
XXX2: ['@nutflow/nf-form-elp'],
XXX3: ['@smallwei/avue', 'avue-plugin-ueditor'], // 头号目标 1
XXX4: ['@nutflow/nf-design-elp'], // 2
XXX5: ['@saber/nf-design-base-elp'], // 3
XXX6: ['@nutflow/nf-form-design-elp'] // 4
}
}
}
},
上面代码我使用了 Vite 的资源合并功能。在 rollupOptions
的 output
部分,定义了 manualChunks
,并指定了不同的模块名称和它们所依赖的模块。这样做可以将这些模块合并到单独的文件中,以减少打包后的文件数量和大小。这是Vite基于Rollup去整合的,并不是Vite自身的功能,Vite使用了Rollup作为默认打包工具,并且也提供了很多选项来进行一些定制。
在上面的代码中,想必大家也看到了treeshake: true
这个设置
Tree Shaking:
js
// vite.config.js
build: {
rollupOptions: {
// 不考虑模块中导出的属性(也就是对象的属性)是否有副作用
propertySideEffects: false,
// 不考虑模块是否具有副作用
moduleSideEffects: false,
},
}
在上面的配置中,也可以使用 treeshake
选项来配置 Tree Shaking。其中,propertySideEffects
和 moduleSideEffects
属性用于指定哪些属性或模块应该被视为有副作用,从而排除它们不进行 Tree Shaking,以减少最终生成的包的大小。
模块的延迟加载
js
components: {
top: defineAsyncComponent(() => import('./top/index.vue')),
logo: defineAsyncComponent(() => import('./logo.vue')),
tags: defineAsyncComponent(() => import('./tags.vue')),
search: defineAsyncComponent(() => import('./search.vue')),
sidebar: defineAsyncComponent(() => import('./sidebar/index.vue'))
},
异步组件,代码只在实际需要时才加载
优化项目的依赖项
js
// vite.config.js
optimizeDeps: {
include: [
'echarts',
'xxxx',
'xxxx',
'xxxx',
]
}
我们通过optimizeDeps
的配置,Vite就会将指定的依赖项进行预构建,以提高项目的启动性能。这样可以减少不必要的构建时间和资源消耗。可以有效的帮助你更精确地控制优化的范围,以避免不必要的构建和资源浪费
Vite的压缩和服务端的压缩
HTTP 压缩是一种内置到网页服务器和网页客户端中以改进传输速度和带宽利用率的方式。在使用 HTTP 压缩的情况下,HTTP 数据在从服务器发送前就已压缩:兼容的浏览器将在下载所需的格式前宣告支持何种方法给服务器;不支持压缩方法的浏览器将下载未经压缩的数据。最常见的压缩方案包括 Gzip 和 Deflate。
以上是摘自百科的解释,事实上,大家可以这么理解:
HTTP 压缩就是以缩小体积为目的,对 HTTP 内容进行重新编码的过程
Gzip 的内核就是 Deflate,目前我们压缩文件用得最多的就是 Gzip。可以说,Gzip 就是 HTTP 压缩的经典例题。
Vite打包时常见的压缩方式: gzip | brotli
这里我个人比较喜欢使用 Brotli 进行压缩,以下是两者的比较:
-
浏览器支持:Gzip 是一种较为通用的压缩算法,几乎所有现代浏览器都支持。而 Brotli 是一种较新的压缩算法,虽然在压缩比方面更好,但并不是所有浏览器都支持。如果你的应用主要面向现代浏览器,可以考虑使用 Brotli。如果需要兼容性更广泛,可以选择使用 Gzip。
-
服务器配置:使用 Brotli 压缩需要在服务器上进行相应的配置。如果你的服务器已经配置了 Brotli 压缩,可以考虑使用 Brotli。如果没有配置或配置比较复杂,可以选择使用 Gzip,因为大多数服务器都支持 Gzip 压缩。
-
文件类型:不同类型的文件对压缩算法的效果可能有所不同。一般来说,文本文件(如HTML、CSS、JS)在压缩方面效果更好,而已经经过压缩的文件(如图片、音视频文件)压缩效果可能较小。
依赖分析
依赖分析,使用这个第三方库就可以了 rollup-plugin-visualizer
还有二个点需要注意一下:
- 如果你使用了Vite,项目中最好禁止使用
export default
导出一个对象,因为这样做会无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree-shaking 只对使用 export 导出的变量生效 - 最好禁止使用 eval 函数, Vite 在生产环境是默认禁用了eval函数,因为 es 模块默认是严格模式的。所以项目中尽量去除 eval
项目中还有好多点,可以去优化比如在Axios请求封装的时候,可以进行忽略重复请求、重试请求连接等等优化。
这里就不一一进行描述了,我们继续往下探讨~
图片的合理使用
不同业务场景下的图片方案选型是不一样的,比较常见的几种图片格式:
-
JPEG/JPG :
有损压缩、体积小、加载快、不支持透明
使用场景: 背景图、产品图片等
-
GIF:
支持透明度和多帧动画、颜色表有限
使用场景:loading动画、表情包等
-
PNG-8与PNG-24 :
无损压缩、质量高、体积大、支持透明
使用场景:Logo、颜色简单且对比强烈的图片或背景等
-
SVG:
文本文件、体积小、不失真、兼容性好
使用场景:Logo、矢量图形、图标等
-
Base64 :
文本文件、依赖编码、小图标解决方案
使用场景: Logo、小图标等
-
WebP:
与 PNG 相比,WebP 无损图像的尺寸缩小了 26%。在等效的 SSIM 质量指数下,WebP 有损图像比同类 JPEG 图像小 25-34%。 无损 WebP 支持透明度(也称为 alpha 通道),仅需 22% 的额外字节。对于有损 RGB 压缩可接受的情况,有损 WebP 也支持透明度,与 PNG 相比,通常提供 3 倍的文件大小。 Webp是比较年轻的,在使用之前最好是查询一下他的兼容性。 WebP相关文档
代码优化(合并请求、减少DOM)
在工程项目中,合并请求我们从多个角度去考虑:
- 比如上面我使用到的
异步组件
(Vue3的defineAsyncComponent
) - 将应用代码拆分为多个模块,按需加载,可以使用 VueRouter 的动态导入功能或者 Vite 的模块延迟加载功能来实现
- 使用 HTTP/2,如果你的服务器支持 HTTP/2 协议,它可以自动合并多个请求,减少网络传输的开销
对于减少DOM操作,可以从:虚拟DOM、事件委托、少修改CSS的属性避免重绘、各种条件渲染等等,这里就不再叙述了~
服务端渲染方面(SSR)、CDN加速
先说CDN:
CDN(Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响。
CDN 的核心点有两个,一个是缓存 ,一个是回源。
"缓存"就是说我们把资源 copy 一份到 CDN 服务器上这个过程,"回源"就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。
CDN通常用于存放静态资源,而业务服务器则负责生成动态页面或返回非纯静态页面。业务服务器像一个车间,通过计算来产出所需的资源;而CDN服务器则像一个仓库,只负责存放和传输资源。静态资源是不需要计算即可获取的资源,如JS、CSS、图片等;而动态资源需要后端实时生成,如JSP、ASP或依赖服务端渲染的HTML页面。
服务端渲染:
想象一下你去一家餐厅用餐的情景。服务端渲染就像是你坐在餐厅里,服务员会为你准备好一道道的菜品,然后端上桌。你只需要等待菜品准备好,然后直接享用。这种方式下,每次你点菜时,服务员都会为你准备好新鲜的菜品,然后一次性端上桌。这样,你可以立即看到菜品的内容,但是每次都需要等待服务员的响应。
而单页面应用则像是你去一个自助餐厅。在自助餐厅里,你可以自由地选择各种菜品,然后自己动手取食。每个菜品都摆放在不同的区域,你可以根据自己的喜好选择想要的菜品。这种方式下,你可以自由地选择和切换菜品,但是需要自己动手去取食。
在网页中,服务端渲染是指在服务器端生成完整的HTML页面,并将其发送给浏览器进行展示。当你访问一个服务端渲染的网页时,服务器会根据你的请求动态生成页面的内容,并将完整的HTML页面返回给浏览器。这样,浏览器可以立即展示页面的内容,但是每次页面切换都需要向服务器发送请求,等待服务器响应。
而单页面应用则是在浏览器中加载一个初始的HTML页面,然后通过JS动态地更新页面的内容。当你访问一个单页面应用时,浏览器会加载一个初始的HTML页面,然后通过JS来处理页面的切换和内容的更新。这样,页面的切换是在浏览器中进行的,不需要向服务器发送请求,但是初始加载的速度可能会较慢。
总结起来,服务端渲染和单页面应用是两种不同的页面渲染方式。服务端渲染可以立即展示页面内容,但每次页面切换都需要向服务器发送请求。而单页面应用可以快速切换页面,但初始加载可能较慢。
服务端渲染典型的:JSP、PHP、Nuxt、Next等等
单页面:React、Vue等等
服务端渲染,可以很好的支持SEO、尤其是新闻类的网站基本都是使用了服务端渲染的方式,方便搜索引擎去搜索网站的内容。
服务端渲染虽然很快,但是如果你的服务器级别不是特别给力话,再加上在当今互联网时代,用户使用的浏览器数量确实非常庞大,而一个公司的服务器数量相对较少。如果将所有浏览器的渲染压力集中到这些有限的服务器上,显然会给服务器带来巨大的负担。(当然、目前也有很多技术去优化:负载均衡、各种缓存(Redis)技术、异步处理、云技术服务等)具体使用什么技术需要综合考虑一下的~
浏览器缓存机制
浏览器缓存是一种简单而有效的前端性能优化手段,可以减少网络IO消耗,提高访问速度。Chrome官方解释了缓存的必要性,指出通过网络获取内容的速度较慢且开销巨大。大型响应需要多次往返通信,这会延迟浏览器获取和处理内容的时间,增加访问者的流量费用。因此,缓存并重复利用之前获取的资源是性能优化的关键。
浏览器缓存机制包括四个方面,按照获取资源时请求的优先级排列如下:
- Memory Cache(内存缓存)
- Service Worker Cache(Service Worker缓存)
- HTTP Cache(HTTP缓存)
- Push Cache(推送缓存)
其中,Memory Cache对应内存缓存,Service Worker Cache对应Service Worker缓存。此外,还有从磁盘缓存(from disk cache)和从内存缓存(from memory cache)获取的资源。
HTTP缓存是我们日常开发中最熟悉的一种缓存机制,包括强缓存和协商缓存。强缓存具有较高的优先级,只有在强缓存失效时才会使用协商缓存。
强缓存和协商缓存的具体细节这里不再详述了。
Chrome官方提供了一张清晰权威的图示,展示了浏览器缓存机制的流程。
在设置缓存策略时,如果资源内容不可复用,可以直接设置Cache-Control为no-store,拒绝任何形式的缓存。否则,可以考虑是否需要每次都向服务器进行缓存有效确认,如果需要,将Cache-Control设置为no-cache。然后,根据资源是否可以被代理服务器缓存,设置为private或public。接下来,根据资源的过期时间,设置相应的max-age和s-maxage值。最后,配置协商缓存所需的Etag、Last-Modified等参数。
浏览器缓存机制和缓存策略涉及许多知识点,这里也只是简单的概括了一下。
除了上面说的,我们还可以使用浏览器的Lighthouse去进行性能分析,然后"对症下药" 这里就不再过多介绍了~
完。