引言

在当今用户体验至上的数字时代,前端性能已不仅仅是技术指标,更是直接影响用户留存、转化率和产品口碑的关键因素。然而,面对层出不穷的性能优化文章和碎片化的优化技巧,很多开发者陷入了”知其然而不知其所以然”的困境——我们熟悉各种优化手段,却难以构建系统化的性能认知体系。

本文旨在打破这一僵局。通过深入分析前端性能的本质问题,我们提出了一套全新的六层性能优化模型,该模型将从前端工程化构建开始,逐层深入至网络传输、应用加载、浏览器运行时、JS执行引擎,最终回归用户体验与业务场景,构建起完整的性能优化知识图谱。

不同于简单的”优化清单”,本文将揭示各优化层级的因果关系和底层原理:为什么Vite能比Webpack更快?微任务如何阻塞主线程渲染?隐藏类怎样影响代码执行性能?更重要的是,我们将探讨如何根据不同的业务场景,在性能与成本之间找到最佳平衡点。

前端性能优化模型

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2cS29cdTAwMWLXXHUwMDE1x/f+XHUwMDE0hrqtJvfec5/Z6UWJMSXKXHUwMDEyLUpcblx1MDAwMoGPIUWJ4nMoSlxuvCm6aIMgaJFcdTAwMTZcdTAwMDWKomhWLbrrplxymlx1MDAxYf0ysZN+i55Ly+SQM1x1MDAxNIfkWGZcdTAwMDBasGHNgzwzc/73d/73MZ8/efp0xbttuCtcdTAwMWY/XXFvXG65aqXYynVXfm63X7utdqVew12s93u73mlcdTAwMTV6R557XqP98UdcdTAwMWZccs5wXG71q7dnuVX3yq15bTzuU/z96dPPe//inkrRnpsrJry2UDp1mThuqmRcbprp7bXeqb2D3lx1MDAwNdNyXHUwMDBiXq5WrrqDXTe4nSvlMFxyzHCuhaRcdTAwMDKgv/tcdTAwMTZ3UyCOYZpKQ7XUWoDs7+5Wit45XHUwMDFlorR2XGJcdTAwMDPJKaOSSd9cdTAwMDecu5XyuYeHXGImXHUwMDFko7nkXHUwMDAyXHUwMDE0kWBY/5C3XHUwMDExffyU9Le0vVb90t2oV+stXHUwMDFi9s+oa39cdTAwMDZB53OFy3Kr3qlcdTAwMTVcdTAwMDfH5ERRl0qDY0qVavXQu+19Mt5lvKMrI5+fvVx1MDAwZp6NbFx1MDAxZndcdTAwMTZ+Yfm85rbtU6D9rfVGrlDxeveJXGauwEbXSFx1MDAxNntcdTAwMGbss0FMrdyVm7RPrNapVvubK7Wia5/DSp7mjoa+r1a8/753z3vwMOF+y8tB9K5rP5pRQlx1MDAxNdOE8v6eQdJRXCJHt+7Va71cdTAwMDRkWlx1MDAxOOBU6EFcXO1NTDyv96mlXFy17Vx1MDAwZVx1MDAxZYBccm1rkJRDl9NpXHUwMDE0c29PokoyfNJGXHUwMDEwwlx1MDAwN3e5Wqldjp5TrVx1MDAxNy57p3itjvvEd10jib7RLOez+nKt412cuo1cdTAwMTNcdTAwMDMklTuJnOiCalx1MDAwN1xmprjmijDG9FCeM1x1MDAwZY7WzCYxl8Joo1x1MDAwM3nOKXdcdTAwMDBQXG5EXHUwMDE4geebYJ4r4nBCXGLjeCyRTMSc5qWSWzDmp57m+fmzXHUwMDFjgEhKVGiSUzouyamkUlx1MDAxM6JmSfJP+9FccuL05Zzn3nj9y/Kl7IW3385fd49cdTAwMTN77qla1cebbl4/X+lcdTAwMWb38v5/n43Xj1x1MDAxMtr4LvRB/fQu4Vx1MDAwMVx1MDAwMYVHXHUwMDEzXHUwMDEw0NDF9LQjjXCE5NJoJZVcdTAwMDbFhrUjpSNQO1x1MDAwMrB9l0BYQDqSXHUwMDA0tVx1MDAxMrc6vFau1m7kWvjAfupcbimEK2To8HspYGuvsLlSoEO0oMe291RwxpFcdTAwMTK+I6bQwlx1MDAwM+kqXHUwMDEwQD6KTE7XQfbZrMPL/+HVb3/47k+v//FcdTAwMGLfU6zXvMPKXVx1MDAwZlRkaGtcIndVqdrbLoY+Z61aKdtcdTAwMWKwUsCQ3daK/zZ4XHUwMDE1LLH6XHUwMDA3XFxVikU/KVxu+KG5Ss1tJaMgp96qlCu1XFw1Mz72XFzHq1x1MDAxZrjtt9FbwPnvjbvzTlxy1GHiXHUwMDAx5VarjfVOerWVXHUwMDA3tXlwlUxcdTAwMTaLnfSLmNBHJe9Ri1x1MDAxOCpcdTAwMDRTwFx1MDAwN63NXHUwMDEyfTFcdTAwMGJ7e272SYJtMGHBUq73XGbG6l1pxbGVoIMj3jv76o1cdTAwMWRmjvb3L8zOafmgda7hwlx1MDAxY03FPsE4tm7xsC88mrnZR7VyqM17XG5cdTAwMTgy4WrJvrdbZ5bIXHUwMDE4XHUwMDEzXHUwMDE0XG4/qjhcdTAwMDDazjD4gVx1MDAxYSdcdTAwMDbB0etcdTAwMTJcdTAwMWG/15k6X1x1MDAwM/B78+dfvv7u31x1MDAwYlx1MDAwMr9cdNBcdTAwMTmFX0js8cDPJFx1MDAwZlx1MDAxYvuNW7JVTub3V4/bW8nLTmJcbvhJh9v+XHIjKIp0XHUwMDAwN3vngIFcdTAwMDPSUlx1MDAwZkBcdTAwMTnGXHUwMDA3d3KJvnh1XZybfFx1MDAxNE2f0cTvL3y2z/foXHUwMDAytS6jUlxioVx1MDAxZZF9peNcdTAwMGXxrqqQu7zM61x1MDAxNC8nXHUwMDFi7YPydL5PXHUwMDAy/o2HfeHRRGJcdTAwMWY4tthgeN/RhupcdTAwMTHxcOlcdTAwMTBcdTAwMTSPXHUwMDE22KJcdTAwMWG8wUv43W+dVSRudPZpSSnT4XUghYBE+lx1MDAxZH3YQFx1MDAwMeMybt+nJGVmmnRccqDv9Vx1MDAxN3/58dWrXHUwMDA1Qd9cdTAwMDTkjKIvJPZ40Ld7UVx1MDAxNqfiprWT9XKnN6lCZn3jXHUwMDBlYvJ9nDBcdTAwMDd3XHUwMDE4XCI4w9qFkEHH/Vx1MDAxMn7x6rpcdTAwMTRcdTAwMDP8qOzd4HD4je3zlMrYwuYx+zyr7Uq7226a+lb1dJV2S8+LN7nmlOzD+jwm31x1MDAxN1x1MDAxZU1cdTAwMTT26Yd8XHUwMDFmZ7hcdTAwMTfFg1x1MDAxMaPx0zSoXHUwMDFkvUTfNFx1MDAxMilHR1x1MDAxZsXk0Eyx8EEuXHUwMDE4X1xiXHUwMDFhSVx1MDAxMFNcdTAwMWNmKlx1MDAwNCd1epop8jVcdTAwMDC/XHUwMDFm//ubXHUwMDFmv/nyzVx1MDAxZv65IPybwJ1R/oWHXHUwMDFmXHUwMDBmXHUwMDAyO955/e7O1E9Tx3snee/koprfzsaFQM2Qb0SAXHUwMDEyjFxihSZhicD3pO/z+Vx1MDAxMWiAXHUwMDE4bJCDo1x1MDAxYT1cdTAwMDRcdTAwMDb6gPrdPUwpjcqfqeSdXHKBz+T5yYvNZu4urzJnzXb28iB1kJlcdTAwMTKBktGIw+aTXHUwMDEwXHUwMDE4XHUwMDFlTVx1MDAxNFx1MDAwNKqHXHUwMDEwKIhw0I1cdTAwMGJuXHUwMDE0hktkcMTcnzdLXHUwMDA2TtZIZVxuXHUwMDA2XHUwMDEyTlx1MDAwNONChHV9Uj7W/2FcdTAwMTWpsaVcdTAwMTJm8Vx1MDAxOPjm139FiLz+z+/ffP3VgmBwXHUwMDAye1x1MDAwMj2g464gXHUwMDFlXHUwMDEydsvbq5u5i/Tzxi6/Sdc7nVx1MDAxZHc7XHUwMDE5XHUwMDEzXHRxu6OtyTCSUkNcdTAwMDZcdTAwMTm+5GC8XHUwMDFhr83PQU60XHUwMDA2tHShXHUwMDFjXHUwMDE0MLq1L31i0Fx1MDAwYlx1MDAwMpbOj1x1MDAwN8IuTSTp3YvN9E6uRLYyrcbV4amcXHUwMDEyhJpy8Otldlx1MDAxMIZHXHUwMDEzXHUwMDA1hPxBXHUwMDEwau5cYtSOtj9cdTAwMDZY0Fx1MDAwYtJlP+hUXCKpTzNcdTAwMDaIlVx1MDAwN1x1MDAwNSlcdTAwMDJj3z01jFx1MDAwNaFcdTAwMDTKOVaGsXNw7jHAXHUwMDFmfve3N7/61/evvv7f37/8/tuvvv/2j6+/+GZBgDhcdTAwMDFBgfkwXHUwMDEzLyUmMt6cXFw8k82EOCpXd0rXifxG6ZNWZDJcdTAwMWFcco4h3HBjXHUwMDE4XHUwMDAybGSUg1x1MDAxYeVIXHUwMDAyRlxuzohUQINcdTAwMWWR2kqYcDu+XGKMI2SDYlx1MDAxN8bRmFx1MDAxZkJpQVx1MDAxNFWEXHUwMDBmXHUwMDFh75joWCiwovqpXHUwMDBivzk/XHUwMDFkNVx1MDAwMXyUkoa2XHUwMDA3cvzkUJQvtTPUXHUwMDFmcZSQ19PyqLHFjq7L63meZ1x1MDAxYt1yujNcdTAwMTVcdTAwMWTRemmIySaGR1x1MDAxM4GOWPlLXHUwMDA3XTbD/DZCYJUxJCBGrU9cdTAwMTTY3Fx1MDAwMkchUVx1MDAxOVx1MDAxYyZcdTAwMDTjKDxPgtHYgipNg1x1MDAwMlqWk6GCaUUnJUPLiPVcdTAwMGJcclWGXHUwMDE5O1vGMDxF6plGXHUwMDEwJixccpBcdTAwMWPENFObXHUwMDAzqExs7C9cdTAwMDRcdTAwMTVcdTAwMWbGzyhcdTAwMTWHoo5cdTAwMDeAibNEQTFcdTAwMDWlQvbZzlx1MDAxYc9kXHUwMDBia5tuZFx1MDAwMFJcdTAwMDSXY1x1MDAwMFDDhipcdTAwMWUkoHSwvDVoXHUwMDBlkV2cXHUwMDA09btcdTAwMDRgXFx6bs9cckCFXGJjXHUwMDEywt2hXHUwMDBlXGaZvJO5NpQrXHUwMDBlM82Km1x1MDAxMX93nXTy5vxsXHUwMDE32Hru5GyrojrqZrqBwjjxXHUwMDE3XHUwMDFlTVx1MDAxNPzhjXOsrybo/jj1XHUwMDE1/2/pXHUwMDA3aFx1MDAxZIFcdTAwMTiBkdpcbjO4Olx1MDAwMpRcdTAwMDPG0lx1MDAwZpQ2Q1x1MDAxZCdL+j2sXHUwMDE2Lzr9qFx1MDAwMYlcdTAwMGZA69BcdTAwMDF0NlZcdTAwMTfMdrMq5l+DXHUwMDEzj1Gcn36pxaDfXHUwMDA09ozSL1x1MDAxNT/93Fx1MDAxY1x1MDAxY+1cdTAwMDOcuTK3u3fUqKfXa4nTKeyfdGyvXHUwMDE411jlIJtGXHUwMDE2N2mrTlx1MDAwNYpSxbB+NcFcdTAwMDWwS/rFpefO3PSjKHDC/UP8vkXXbPx6KKLQM1xuIWZS+Wz4y2qVzjYyqbuDvePVTvfkrpY/11x1MDAxZlxmf+HRRHJ/zC5cdTAwMTG3zaQmhEg97P6AcEfbKaKEXHUwMDE4NHYm2HtcdTAwMDLC0cpcdTAwMThhKJGcXHUwMDBiWJq/+5gmyuV6mm5SYLbhXHSfLyrHuj9KXGZcYiBY2CxcdTAwMWP+NlKHi4C/XHTwXHUwMDE5xd9Q1PHgbz975lVlp5ButitJnW00y89cdTAwMGWjL1x1MDAwZUTzx1x1MDAxYzukh8olXHUwMDA2XHUwMDE1PGz+mOZcdTAwMGWKXHUwMDEypFLA7btcdTAwMDWW+Htveu7Ojz+JSrWzRMNkzsZPXHUwMDEzXHUwMDA10JhcdTAwMDF44uPh74itka3r1id7N6eNplTtVFx1MDAwNmqtXHUwMDBmhr/waKK5P4bJzYVdsaGAmWH7XHUwMDA3hDpac8GlXHUwMDE0XHUwMDA22Vx1MDAwNiGLa4VDkX+GSWW7Ppf2711MXHUwMDEz9XJcdTAwMTOdf0BASaHCp4yOXyfPXGLlhs3W+fl+6ZfJJFx1MDAxN4F+XHUwMDEz2DNKv6GoY+r6TJZcdTAwMTNcdTAwMTlGss8rqrXVdcvZq0YjXHUwMDFmmX5Ga1x1MDAwN1x1MDAxODF2XHUwMDAykzSCXHUwMDBlXG6hnnqVcFx1MDAxNFpcbmOdofDPMVxcwi9uMd/OXHUwMDBmP2FXNoFcdTAwMGVdI8HGT1x1MDAwNbBccrOyT+bx4NdcXFP79ebRmtndvTiry1x1MDAxYu8445pcdTAwMGZcdTAwMDa/8GhcInk/Klx1MDAxZILUXHUwMDEzwoLLN8W2p1x1MDAxZVx1MDAwM1x1MDAwZUXfh1gzQitcYnl3XHUwMDE4N1x1MDAwZUf0aa7s28OIz2Us2fewXFzuorNPXHUwMDEw9N9YYYROXHUwMDE1XHUwMDFkP/Bnl7ZwzmjsU0VjgN96Zlx1MDAxMeA3XHUwMDAxPVx1MDAwMfj5o45cdTAwMDd+rXLmsPvJeiuxfaArJ0ZvPkufkqnH/SRhXG6LVGZG6cdcdTAwMWOlONakVtuGh8xrW9IvXHUwMDFlOTMyN/24UsLOX1xuhZ9cdTAwMWE/IVxcY+VjhIBH7Pks3r6oN1x1MDAwZsn1xlx1MDAwYlZvrJ7eZc9yWn0w+oVHXHUwMDEzzfpJRyjRe69cdTAwMGVcdTAwMTA9olx1MDAxZUNcdTAwMWNGhJFcdTAwMDQrR1wiQjpO7KxcdTAwMTdqjFwiisDS9kXXXG6Njj7Nsa4gXHUwMDEw+nY0XHUwMDE2rFx1MDAxM1x1MDAwNytcdTAwMDVcdTAwMDVcdTAwMTb9jC7goF9cIrm5XGLom1x1MDAwMJ7AlFx1MDAxN3/UMaGvmN69bZ/wXHUwMDFisdXaOyHX8rLZvZrC91x1MDAxOVx1MDAwN1xiQ1eAyrPT1oa0yyVxUN32zYu2S0ctyff+1Mxi8H2cXHUwMDEwO3s3rHOHqfFcdTAwMGJcIuxEUSx8XHUwMDFlc2XgTqHyvJRMlk/bOzxvXG53cCzWLj9cdTAwMTj6wqOJZPxcdTAwMTg4RGD+21nRxv/6ydu3tYgjUETKzuiU/kVqvjkvS+M3k14gOv3s0KxcIthETYs/u6STcL6Ag37JvYWY8zJcdTAwMDE+o/hcdTAwMWKKOlx1MDAxZfxdN7peZS112Hq2+aIgWsfJZvcs+lvf3zk/LoDYLrORnlx1MDAxYtyE6tZcdTAwMDY0cEWMXGbOWVvyLy4987n5x1xmXHUwMDAxLGRCh/xcdTAwMWV4JShBkNiFwI845neWyFS9dOX8Ondaeb4mM9tu+tr9YPRcdTAwMGKPJpLxXHUwMDAzzH4lXHUwMDE0XHUwMDAzkJRg+lx1MDAwZqtHXHUwMDEzhFx1MDAxYrFcdTAwMTPGXHUwMDE044KHvVx1MDAxN40trd8sYlx1MDAxMVPAT0pcdMS+jzxcdTAwMTR+43VcdTAwMDGMSUY1LOKYX2J9XHUwMDEx6DeBPcFBP3/YMa34O+g2d9pcdTAwMWRJ0/vZ3e16m1x1MDAxNFx1MDAwZVx1MDAxYjQy/kbd33DPjVx1MDAwMONIwplcdTAwMDT0gJyHXGZbMKZcdTAwMWQp7Ji/oJJo4suVJf6mU7ScXHUwMDFif1x1MDAxYZ+mXHUwMDE04Vx1MDAwYoCBjNW5sm/uXHUwMDAy/3z8946/gltfLW1cdTAwMTihqm5qtZA6zKvs5vpcdTAwMDfDX3g0kcxcdTAwMWZXjlx1MDAxNsbecyqH5lx1MDAxNPbkI5Rd78fRXHUwMDE4ovsjLPguXHSsPlx1MDAxZFx1MDAxNFx1MDAxOVwi0r53XHUwMDE3TeDS/d3HNFEuKjpcdTAwMDDx/lx1MDAwMlb3obpgYuywXHUwMDFml1JpzmJfXHUwMDE4Pz/+1vHLqu7TXHUwMDFl81x1MDAxNoCCXHUwMDEzXHUwMDEwNErB0OinhuGT++ZhJddoXHUwMDFjeniH+23lynXF7a6HJr79Y5uZXjtgXHUwMDEz3+01sS+fvPw/P51cdTAwMTAqIn0=网络层构建层加载层运行时层执行引擎层用户体验与业务层FCPLCPCLSTTITBTFIDINPTTFBBundle Size

构建层(前端工程化)

如果简单分析一下,构建打包层面常见的优化手段有 tree-shaing、压缩和代码分割等,这些主要是优化 bundle 体积,影响的是网络传输,所以应该被归类到网络传输层。但是实际上,由于前端工程化的多样性和复杂性,除了上述的打包优化手段,还可能涉及到工程化架构、代码组织方式和微前端等方面,更重要的是,前端性能优化不单单的对用户来说,对于开发者来说,冷启动和热更新的性能也需要关注,因此构建层单独作为第一层。

生产性能

减少 bundle 体积

Bundle 的体积将直接关系到页面加载速度,bundle 体积越小,那么网络传输速度不变的前提下,网络传输用时将减少,从而提高用户体验。

在前端工程化中,几乎所有主流打包工具都支持对 bundle 体积的分析和优化,其中最常用的优化方式如下:

优化方式说明
代码分割可以显著降低首屏加载体积
Tree-Shaking自动剔除未使用的 JS 代码
轻量级依赖例如用 dayjs 替换 moment
代码压缩如删除无用的空格和换行符,精简变量名等,从而使 bundle 体积减少
按需引入Ant Design、Element Plus 等均支持 babel-plugin 按需加载
提取公共 vendor. js将公共依赖提取为 vendorjs 单一 bundle 文件加载,在微前端中,可以避免后续子系统重复加载

优化开发性能

开发性能主要需要关注两方面:启动和热更新(HMR),其中启动又分为冷启动和热启动。现代大部分主流打包工具(如 vite、rspack 等)其实性能都已经非常优秀,然而某些情况下,还是可能会遇到开发性能问题,比如老项目中采用 webpack 打包。

Vite

Vite 启动速度和热更新速度都很快,这得益于 Vite 底层多方面的优化与设计,关键的一点在于,Vite 在开发环境下并不会将所有文件都打包成 bundle,而是借助 ESM 特性进行按需加载,这是 Vite 那么快的关键原因之一。

借用 Vite 官方的两张图可以很形象地展示两者之间的区别。

此外,Vite 还通过使用 Esbuild 预构建依赖、使用 HTTP 缓存等多种方式来优化启动和热更新速度,这使得 Vite 的性能通常要比 webpack 要好。

Webpack 优化:Vite 所带来的启发

虽然 Vite 性能非常好,但是在某些情况下,我们还是不得不用 webpack,比如在开发一些老项目时。

webpack 比 vite 性能要差的核心原因在于,webpack 是基于 Bundle 打包的,而 vite 在开发环境下是基于 module 按需打包,这使得 webpack 每次启动都需要打包大量文件,而 vite 则会在第一次构建(或者修改依赖或配置时)预构建依赖,只需要打包使用到的源代码。

因此,如果我们能够让 webpack 少打包写文件,只构建我们所需要的文件,那么速度自然就能快起来了。

突破点在 router,以 vue-router 为例:

// 伪代码
export routes = [{
	path: '/a',
	componet: /* webpackChunkName: a */ ()=>import('@/views/a.vue')
}]

在 Vue 实际消费这个 routes 时,我们可以提前拦截处理

import openRoutes from "./route-config"
 
// 这里简单展示思想只处理最外层,实际开发需要深度遍历树
const handledRoutes = routes.forEach((route)=>{
	const component = route.component // 保存原组件
	if(!openRoutes.includes(route)){
		route.component = ()=>{
			// 空组件,优化性能
			return {
				render(h){
					return h('span', null)
				}
			}
		}
	}
	
	return route
})
 
export default handledRoutes

这种方式的缺点在于需要手动配置打包的路径,并且每次修改后都需要重启 webpack,但是后面可以通过 webpack 插件来简化这一过程。

网络传输层

当用户打开一个页面时,浏览器会自动发起对应 HTML 文件的请求,浏览器中的 HTML 解析器首先会加载并解析 HTML 文件,构建成 DOM,与此同时 HTML 解析器和预解析器还会加载 HTML 中声明或 Script 中 JS 动态创建的资源(CSS、JS 、音视频等)。

即使在构建层已经尽量压缩了资源的体积,但是如果网络传输中存在网络延迟过高等问题,也依然会影响系统性能,因此网络传输层也是我们关注的重点。可以通过 TTFB 等指标来分析网络传输层的性能,我们可以通过 CDN、HTTP 缓存、keep-alive长链接等手段来优化这一层的性能问题。

总的来说可以分为两个层面:

  • 网络层
    • CDN
    • HTTP 缓存
    • 长连接
  • 前端层
    • 队头阻塞(浏览器限制)
    • 请求去重(如节流)

对于前端自身的网络层限制,前者可以借助 HTTP 2 和请求队列等方式优化,后者则可以借助节流和防抖等技术缓解。

此外,在某些场景下,还可以采用离线缓存的方式来优化性能,比如 C 端常见的“未读通知”,应用首次加载时,立即加载缓存中的未读通知数量并展示,同时发起网络请求,等得到响应后再进行更新,这适合要求首屏尽快渲染的场景。

总结来说,网络传输层优化的本质,就是提高网络传输速度、缓解队头阻塞问题、减少不必要的重复请求

应用加载层

应用加载层可以理解为包含从 HTML 被浏览器加载到首次渲染这一整个过程,概括起来就是首次加载和渲染。

事实上,当从 HTML 被浏览器解析开始算起,就可以看作是已经进入到了浏览器的运行时一层中,但是为什么还是单独拆分了“应用加载”这一层?

这是因为对于很多(尤其是toC)应用来说,首次渲染太重要了,它甚至直接决定了用户是否愿意使用这个应用,毕竟用户等待的耐心是有限度的;此外,也是出于复杂度的考虑,加载层有一些专门需要关注的性能优化点需要重点关注。

关键路径优化

什么是关键路径?简单来说,关键路径是指从页面加载到最终渲染完成所需要的最小步骤。

比如说入口文件 HTML,它是页面加载解析的开始,没有它整个页面都无法渲染,因此入口 HTML 文件的加载和解析就属于关键路径中的一环;像 icon 图标这类资源,即使它没有加载出来,也不妨碍整个页面主内容的渲染,那么它们就不属于关键路径。

从浏览器的角度来看,关键路径核心步骤是:

  1. 加载 HTML
  2. 解析 HTML 得到 DOM
  3. 下载 CSS 并解析得到 CSSOM
  4. 样式计算
  5. 布局
  6. 绘制
  7. 合成渲染
  8. 页面出现内容

这部分其实是渲染管线,具体在《运行时》一章中具体介绍

然而实际开发中更加复杂,例如一个大型 B 端应用,采用 SPA 架构,并使用微前端,而且还可能集成了各种 SDK,那么加载流程会更加复杂,一个可能的加载流程是:

  1. 首先加载 HTML
  2. 下载核心静态资源(JS/CSS)
  3. 加载基座/框架并初始化
  4. 拉取用户信息/权限/菜单/配置/国际化等
  5. 动态生成路由,注册子应用
  6. 初始化各类 SDK(埋点、监控、图表、编辑器等)
  7. 匹配路由并加载当前子应用
  8. 子应用渲染

在这个关键路径中,需要执行到最后一步,才会渲染页面核心内容,很显然这太慢了,这也为什么需要重点关注应用加载层的原因之一,因为关键路径会直接应用首屏加载性能。

分析这些关键路径,可以发现,卡点主要体现在三方面:

  • 网络阻塞
  • 主线程阻塞
  • 关键链路太重太长

因此,优化关键路径的思路也有三个:

  1. 网络层优化:让关键资源加载速度更快一点
  2. 运行时优化:减少 JS 计算,减少对主线程的阻塞
  3. 业务优化:延迟非核心资源的加载和执行,简化关键路径

渲染架构

除了关键路径外,不同的渲染架构的加载性能也不同,如果能够在项目前期就选定合适的架构,可以在日后节省很多优化工作。

前端常见渲染架构有 SSR(服务端渲染)、CSR(客户端渲染)和 SSG(静态站点生成),三者的渲染策略不同,主要区别如下:

渲染架构渲染策略适用场景常见框架
SSR服务端每次收到请求后动态地构建 HTML,服务器通过访问数据库或者发送请求获取最新数据并填充到 HTML 中新闻、官网等网站Next、Nuxt 等
CSRHTML 只有一个容器节点,页面内容主要都是通过加载的 JS 动态创建 DOM 生成的,数据主要通过在浏览器中发送请求获取中后台系统Vue、React 等
SSG构建过程中就生成了后面所需要的全部静态资源,除非重新构建,否则内容不会更新博客、文档等静态网站VuePress、Hexo 等

三者的优缺点如下:

维度SSRCSRSSG
DOM 生成来源HTMLJSHTML
SEO非常好
时效性非常好非常差
首次加载性能非常好
服务器压力非常低
浏览器压力

总的来说,SSG 适合对时效性要求低、对 SEO 要求高的场景、且这种方式性能也非常好,如果对时效性和首次加载性能要求较高,那么通常可以选择 SSR 架构、如果对 SEO 和首次加载性能不在意,且服务器资源有限,则可以选择 CSR 架构。

当然,除了上面三种典型的前端渲染架构外,还有一些新兴的渲染架构,比如 ISR(增量静态再生)。传统的 SSR 每次收到请求都会重新生成一份新的 HTML,虽然时效性好,但是对服务器的压力非常高,而 SSG 只在构建过程中才生成 HTML,此后不再生成 HTML,虽然性能非常好,但是时效性非常差,而 ISR 则是在两者之间取得一个平衡。

和 SSR 一样,ISR 也会获取最新数据填充到 HTML,并返回给浏览器端,但是与 SSR 不同的时 ISR 会采用缓存策略,在缓存过期前,服务器每次收到都会直接返回之前生成的 HTML,当缓存过期后收到请求,服务器仍然会直接返回之前生成的 HTML,但是此时服务器会获取最新数据重新生成 HTML,如果后面再次收到请求,则是返回后面生成的最新的那份 HTML。

性能优化是取舍与平衡的艺术,不可能面面俱到,每种渲染架构都有各自的优缺点和适用场景,根据不同的需求采用不同的架构,才有可能得到最理想的性能。

资源加载策略

避免阻塞

应用加载需要关注阻塞问题,这是因为 HTML 解析器解析 HTML 时,会被 script 标签阻塞,script 的加载和执行都会阻塞 HTML 解析器,影响整体的首次加载速度。避免 script 的阻塞包括使用 defer 和 async。两者区别如下:

  • async:异步加载,并行解析执行
  • defer: 异步加载,延迟到 DOM 解析完后执行

<script type="module"> 默认会异步加载,延迟执行,也就是说下面两种写法效果是一样的。

  • <script type="module">
  • <script type="module" defer>

此外,css 中的 @import 也会阻塞解析,因此在优化首次渲染时需要额外注意。

预加载和预获取

在 HTML 规范中,资源的加载是有优先级的,通常情况下我们不需要特意关注,但是在一些情况下,我们可以利用浏览器提供的 API 来利用资源加载优先级来优化性能。

常见的 API 有 preload 和 prefetch,前者是预加载,可以在应用早期就尽快地加载资源,后者是预获取,可以提前加载后续可能用得到的资源。

举一个简单的场景说明下这两个 API,在一个电商平台的首页,轮播图图片可能比较大,加载时间会比较长,影响用户体验,但是如果设置了 preload,那么就可以确保浏览器会尽早地加载这张图片,减少图片加载时间;首页轮播图的下面是热卖商品链接,如果设置了 prefetch 预获取,那么浏览器就会在空闲时提前加载商品链接中的资源,你点击这些链接时就可以很快地进入到页面中。

// layout.jsx
export default function Layout(){
	return <>
			{/* ... */}
		 	<link rel="preload" as="image" href="/banner.jpg" />
		</>
}
 
// Page.jsx
export default function Page() {
  // 实际上Next的Image组件已经封装了preload,这里只是为了演示,实际写法更简单
  return (
  	<img src="/banner.jpg"  alt="首页轮播图"/>
    <Link href="/shop/xxx" prefetch>
      商品链接
    </Link>
  );
}
 

首屏优先加载策略

应用加载层追求的是首次加载渲染速度尽可能的快,也就是白屏时间尽可能的短,而如果需要做到这一点,除了做好前面的架构选择和减少阻塞外,还得实现首屏优先加载策略。

所谓的首屏优先加载策略,核心就是首次加载只加载首次屏幕上能看到的内容,而看不到的内容延迟加载。一个常见的例子是购物网站,刚进去时只会渲染屏幕视口中的商品,如果刚进去时快速滚动页面,可以发现首屏下面的内容还没有加载。首屏视口中的内容用户最先看到,因此优先级最高,需要尽可能早的加载渲染,而首屏视口后面的内容用户后面才会看到,因此优先级相对较低,延迟加载渲染,这有利于提高用户体验。

我们用 React 来展示一个简单版本的首屏优先加载策略实现方式:

// first.jsx
function FirstComp(){
	return <div className="px-2">首屏内容</div>
}
 
// second.jsx
export default function SecondComp(){
	return <div>延迟加载</div>
}
 
 
// home.jsx
import {useEffect, useState} from "react"
import dynamic from "next/dynamic"
 
const SecondCompLazy = dynamic(()=>import('./second'),{
	ssr: false, // 不参与 SSR
  	loading: () => <div>Loading...</div>, // 占位符
  	})
  	
function Page(){
	const [isLoadComp, setIsLoadComp] = useState(false)
	useEffect(()=>{
		setTimeout(()=>setIsLoadComp(true),  1000)
	}, [])
 
	return <div>
	  <FirstComp />
	  {
		  isLoadComp && <SecondCompLazy />
	  }
	</div>
}

运行时

当应用加载完毕且完成首次渲染后,就可以认为进入了运行时阶段。浏览器存在有一个执行栈和事件循环模型,这是 JS 执行和渲染的基石,事件循环模型每个周期都会检测是否需要重新渲染,当用户与页面进行交互时,可能会触发重新渲染,则会重新前面的渲染管线中的操作,不过不需要重新加载 HTML。

运行时的主要瓶颈在于主线程和渲染管线,主线程除了执行 JS 外还负责 GC 和渲染的部分任务,因此如果长任务阻塞主线程,那么就很容易造成丢帧卡顿。另一个瓶颈在于渲染管线,渲染管线是由主线程、合成线程和 GPU 共同完成的,即使主线程未被阻塞,大量重复渲染也依然可能影响合成线程和 GPU 的性能,从而导致卡顿。

除了主线程和渲染管线外,还需要额外关注内存管理,常见的内存问题主要有两类:

  • 栈内存溢出
  • 堆内存溢出

可以导致内存问题的原因有很多,比如内存泄漏、缓存队列溢出等,但是一旦出现内存问题,轻则导致页面卡顿,重则直接页面崩溃,影响用户体验。

主线程

JS 是单线程的,通过执行栈和事件循环模式实现同步和异步任务的调度和执行。JS 单线程模型简化的开发复杂度,但是同时也带来了一个问题:阻塞。浏览器中主线程通常是“身兼多职”,除了执行 JS 任务外,还需要负责垃圾回收、页面渲染(部分)等任务,因此阻塞主线程会影响的影响非常大。

好在,JS 引擎一直在尝试缓解这个问题,例如在垃圾回收中,V8 引擎通过空闲时间 GC、并发、并行等多种技术来进行优化,减少对主线程的阻塞。但是如果开发者忽视主线程的 “负载边界”,写出不合理的代码,依然会让引擎的优化效果大打折扣,甚至直接触发主线程阻塞,导致页面卡顿、交互延迟等问题。

主线程优化的核心在于避免主线程阻塞,对于避免主线程阻塞,很多人都认为主要是避免长任务,但是实际上,JS 任务可以分为同步任务、宏任务和微任务,同步任务的优先级最高,会立即执行,微任务和宏任务属于异步任务,调度时机受到事件循环机制的控制。在一个事件循环周期内,首先会执行一个宏任务,然后执行完所有微任务,之后检查是否需要更新渲染。

因此对于主线程来说,微任务和同步任务都有可能会阻塞主线程,例如在微任务中创建新的微任务。下面代码会阻塞主线程,导致页面卡死崩溃。

function createMicroTask(){
  Promise.resolve().then(createMicroTask)
}

主线程的优化关键在于非阻塞,在不同维度有不同的处理方法,下面列出了一些常见的优化思路。

优化方法原理
Web worker新开线程,避免单一线程阻塞
时间切片将长任务拆分成多个更细的任务,然后间隔时间插入到主线程中执行,避免长时间连续性的阻塞。
防抖/节流缓解高频事件的执行
时间切片可以采用 setTimeout 定时器、MessageChannelrequestAnimationFrame 或者 requestIdleCallback,但是注意不要采用微任务 API,例如 queueMicrotask,因为事件循环机制一个周期内会执行完所有微任务,不符合时间切片间隔插入主线程的要求。

渲染管线

渲染管线(Rendering Pipeline)是指浏览器从接收 HTML 到最终转换成屏幕中像素这一有序、协同、流水线式的处理步骤。对于前端来说,渲染管线尤为重要,如果渲染管线被阻塞,那么浏览器就会因为没有及时完成渲染帧而出现“丢帧”现象,从而导致页面卡顿。

不同浏览器的渲染管线步骤并不完全相同,但是大部分浏览器的渲染管线通常都包含下面这几个步骤:

  1. 解析:浏览器加载 HTML,HTML 解析器解析 HTML 并转换为 DOM 树,预解析器非阻塞性地加载资源,CSS 解析器解析 CSS。
  2. 样式计算:CSS 引擎遍历 DOM 树并应用 CSS 规则,生成渲染树
  3. 布局:计算每个元素的宽高、位置等几何属性
  4. 绘制:CPU生成绘制指令
  5. 合成:合成线程将绘制指令传递给 GPU 进程,由 GPU 进程中的光栅化线程池进行光栅化,最终渲染到屏幕上。

有关渲染管线的具体流程以及常见浏览器渲染引擎之间渲染管线的差异详见深入浏览器引擎 III:Chromium分层合成 vs Firefox WebRender的GPU革命

渲染管线优化的核心在于消除渲染管线中各流程节点的性能瓶颈。我们所看到的页面和动画(如页面滚动)都是经过渲染管线处理后生成的渲染帧组成的。例如常见的屏幕刷新率是 60 HZ,这意味着理想状态下应该在 1s 内生成 60 个渲染帧,也就是 16.66 ms 生成一帧,才能保证页面动画的流畅性。如果因为阻塞或者高强度的渲染导致无法及时地生成渲染帧,就会导致页面卡顿。

下面是一些常见的优化渲染管线的方法,需要注意的是,由于主线程也承担渲染管线的部分任务,因此主线程的优化其实也是渲染管线优化的一部分,不过有关主线程的优化已经在上一节内容解析过,这里就不再赘述了。

方法原理
减少回流、重绘减少重复渲染
减少强制同步布局减少无用渲染
使用 opacity、transform 来实现动画启动 GPU 加速,性能比 position 更好
通过 will-change提升图层单独渲染,并告知浏览器提前做好优化准备,但是要避免滥用
懒加载/虚拟滚动避免短时间大量的渲染
Script 添加 defer 或 async 属性避免阻塞 HTML 解析
content-visibility: auto跳过不可见内容的渲染
contain 属性告诉浏览器将元素视为独立渲染单元,限制其对页面其他部分的影响

内存管理

由于 JS 引擎会自动进行垃圾回收,并没有暴露手动内存分配与回收的相关 API,因此内存问题也很容易被忽略,但是一旦发生内存问题,那么影响也会很大,例如内存泄露很容易导致页面崩溃,影响用户体验。

常见的前端内存溢出问题有两种:

  • 栈溢出
  • 堆溢出

栈溢出是指执行栈中任务数量超出执行栈的最大限制,这通常是大量递归导致的,相较而言比较容易识别和处理,常见的优化方式是将递归改为迭代。

堆溢出则是堆空间出现内存溢出,有两种常见的情况可能导致堆溢出

  • 内存泄漏
  • 消费不及时(生产速度大于消费速度)

内存泄漏是一个很深很广的范围,例如笔者曾经遇到并解决过 Vue Devtools 的内存泄漏问题,最终排查发现是 Vue Devtools 时间轴功能默认开启并长时间深拷贝 Vuex 执行 commit 中的参数导致的,由于 Vue Devtools 将这些数据保存在全局变量中,导致垃圾回收机制无法进行回收。

要避免或者解决内存泄露问题,就必须要了解浏览器的垃圾回收机制和 JS 底层原理,浏览器垃圾回收机制可以参考这篇文章深入浏览器引擎 IV:V8 垃圾回收机制,JS 底层原理可以参考笔者的《浏览器篇》和《深入 JavaScript 篇》,这里不再赘述。

“消费不及时”这种情况也会导致内存溢出问题,例如在 node 服务端如果将日志保存在数据库中,由于数据库是文件存储,存储速度(消费速度)较慢,而日志生产速度较快,导致消费速度小于生产速度,从而导致内存累积,直到出现内存溢出。

又比如在浏览器发送日志的场景中,由于担心频繁地进行日志发送可能导致浏览器队头阻塞,所以可能会采用缓存队列的方式来按顺序、间隔地发送日志,但是如果发送速度小于日志的产生速度,那么就很可能导致内存溢出。

对于“消费不及时”这种内存问题的最佳方法是设立阈值和建立监控,例如上面那个浏览器采用缓存队列发送日志的例子,如果定期地检查缓存队列的长度,当缓存队列的长度超过规定阈值时,则改为使用批量发送的方式并降低发送间隔时间,直到缓存队列的长度降到安全的范围。

在浏览器中,对于堆内存问题的定位和修复通常需要借助 devtools 内存工具,这部分后面会出一篇专门的文章介绍。

一个例子

可以用一个常见的场景来体现着三层性能优化的重要性:长列表

例如在某个移动端社交小程序中需要以长列表的方式展示评论,如果不考虑任何优化,一次性加载所有数据,那么渲染管线会在短时间内承担大量的渲染任务,与渲染管线有关的主线程、合成线程、GPU进程以及其他相关的线程都会受到影响,造成性能悬崖

此时你会想到采用懒加载的形式去优化长列表,但是会发现当加载的节点达到一定数值时,会造成内存溢出,从而导致内存崩溃。这是由于大量节点和相关内存占用(例如事件处理程序)过多,影响了内存管理

你会进一步优化,采用事件委托+虚拟滚动(又称虚拟列表)技术来进一步优化内存。虚拟滚动简单来讲就是通过计算来确定当前应该展示列表中哪几项在屏幕视口中,不再视口中的项由于用户不可见,因此不需要渲染,从而节省内存占用。

例如列表中的每一项高度是 100px,当前滚动偏移量是 1000px,视口高度是 700px,那么此时应该渲染 list.slice(1000/100, (1000/100) + (700/100)) 范围内的元素。当然实际实现时会更加复杂,比如需要在计算好的范围内+-5 来设置缓存区,避免在用户快速滑动时出现由于渲染不及时导致的空白问题,这里就不展开讲述了。

在这个方案中需要监听 scroll 事件来确定滚动偏移量,但是如果你没有采用防抖技术来优化,而是直接在 el.addEventListener('scroll', ()=>{/**/}) 中进行计算,那么由于 scroll 事件触发非常频繁,会导致主线程在短时间内承担大量的计算工作,从而阻塞主线程,因此这种场景下通常需要加上防抖来优化性能。

通过上述步骤的逐步优化,我们可以实现一个简易版虚拟滚动方案。该方案充分兼顾了渲染管线、内存管理和主线程三个层面的性能 —— 正如短板效应所示,唯有三者性能均得到优化,才能最终实现页面的卓越性能。

执行引擎层

在这一层中,你需要深入理解更加底层的知识才能进行更好的优化,可以简单的划分成两层:

  • 框架层
  • JS 层

框架层

深入理解框架底层可以帮忙我们写出性能更好的代码,例如对于 Vue 和 React 这些基于虚拟 DOM 的框架,列表节点通常建议采用唯一固定值作为列表元素的 key 值,这是为了在框架底层进行 diff 时来标识节点,以 Vue3 为例,有 key 和没有 key 两种场景下 Vue 采用的策略完全不一样,性能上也有较大差异。

策略性能
有 key通过key精确复用节点,利用LIS最长递增子序列优化移动接近 O (n)
无 key尽量复用节点,但是由于缺少key,无法精确找到对应的节点最坏情况是 O (n^2)

如果你对key的不够了解,可能会将index设置为key,但是index并不是稳定的,加入中间某个节点被删除,那么同一个节点在新旧节点的key就不相同,导致复用错误,从而影响性能。

除此之外,比如React会提供useMemo和useCallback等API来优化性能,Vue提供computed等API来优化性能,通过了解使用框架的设计思想和实现方式,可以更好地利用框架底层机制来优化性能,写出更优雅的代码。

JS 引擎层

JS 是一个弱类型的解释型语言,通过 JS 引擎来优化、执行,常见的 JS 引擎如下所示:

介绍应用
SpiderMonkey世界上第一个 JavaScript 引擎Firefox 浏览器
V8当前应用最广泛的 JavaScript 引擎Chromium、Nodejs
Hermes专门用于 RN 的 JavaScript 引擎React Native

深入了解 JS 引擎能够帮助开发者在极端性能场景下找到优化突破口,其中隐藏类(Hidden Classes)和內联缓存便是 JS 引擎普遍采用的关键优化手段。简单来说,隐藏类是 JS 引擎在运行时为对象动态创建的内部数据结构,它的核心作用是将动态的 JavaScript 对象 “静态化”,从而让引擎能够像处理静态类型语言(如 Java、C++)中的对象一样高效地访问属性。具体可以参考深入JavaScript II:从JS引擎实现中看JS的运行与优化

以一个例子来对比隐藏类优化生效和未生效两种情况下的性能:

// ✅ 稳定结构,隐藏类可优化
function createStable() {
    return {a: 1, b: 2};
}
 
// ❌ 动态结构,隐藏类未生效
function createUnstable() {
    const obj = {};
    obj.a = 1;
    obj.b = 2;
    obj.c = 3
    delete obj.c
    return obj;
}
 
// 测试函数
function test(obj) {
    for (let i = 0; i < 10e7; i++) {
        obj.a = i;
    }
}
 
// === 性能测试 ===
console.time('稳定隐藏类');
test(createStable());
console.timeEnd('稳定隐藏类');
// 稳定隐藏类: 70.7470703125 ms
 
console.time('不稳定隐藏类');
test(createUnstable());
console.timeEnd('不稳定隐藏类');
// 不稳定隐藏类: 361.491943359375 ms

两者执行所消耗的时间相差足足有五倍,并且随着数据量的增多,两者的性能差异会越来越大。

用户体验与业务层

前面五层是通用场景优化,覆盖常见的性能场景,但是最后一层关注的是用户体验与实际的业务场景。

用户体验

前端是离用户最近的开发,因此前端不能仅仅关注技术上的性能优化,对于前端而言,有一些可以提高用户感知的性能优化方法,这类方法虽然不能够在客观上提高性能,但是可以在用户主观上提高用户体验和性能感知,让用户“感觉加载速度比较快了”。

常见的技术有:

  • 骨架屏
  • loading
  • 占位图
  • 首屏优先渲染

业务相关

性能优化最终要服务于业务目标,脱离业务场景的“极致优化”往往是资源浪费,甚至可能引入不必要的复杂性。因此,在实际项目中,性能策略必须与业务优先级、用户行为和产品生命周期阶段对齐

例如中后台系统,对于中后台系统而言,用户多为企业内部员工,对 SEO 无要求,但需频繁操作复杂表单和表格,用户可能可以容忍稍长的首屏加载时间,但是对运行时响应性能要求较高。

总结

这六层优化模型是一种方法论,它并不能直接告诉你用什么方法去优化,但是可以给你提供优化的方向以及性能瓶颈的判定方法。通过这个性能优化模型,可以将原本零散的性能优化点组织成一个有层次、有因果关系的性能优化体系。