# 18 | 实战痛点4:Vue 3 项目中的性能优化 你好,我是大圣,欢迎进入课程的第18讲。 在上一讲中,我们聊了项目中的权限设计,相信你对Vue项目中如何设计,以及如何实现权限都有了自己的心得。今天,我们来聊一下Vue项目中的另外一个难点:性能优化。性能优化是一个老生常谈的话题,如果你是前端从业者,那么无论是求职的简历,还是晋升的PPT,性能优化相关的内容都是不可或缺的。 那么在Vue项目中,我们应该如何做性能优化呢?下面,我们会先从Vue项目在整体上的执行流程谈起,然后详细介绍性能优化的两个重要方面:**网络请求优化和代码效率优化**。不过,在性能优化之外,用户体验才是性能优化的目的,所以我也会简单谈一下用户体验方面的优化项。最后,我还会通过性能监测报告,为你指引出性能优化的方向。 ## 用户输入URL到页面显示的过程 我们先来聊一个常见的面试题,那就是用户从输入URL,然后点击回车,到页面完全显示出来,这一过程中到底发生了什么? 通过下图,我们可以从前端的视角看到从输入URL到页面显示的大致过程: ![图片](https://static001.geekbang.org/resource/image/95/5b/9550f050235a9bc0a91dc6e33f7e9e5b.jpg?wh=1920x923) 简单来说,就是用户在输入URL并且敲击回车之后,浏览器会去查询当前域名对应的IP地址。对于IP地址来说,它就相当于域名后面的服务器在互联网世界的门牌号。然后,浏览器会向服务器发起一个网络请求,服务器会把浏览器请求的HTML代码返回给浏览器。 之后,浏览器会解析这段HTML代码,并且加载HTML代码中需要加载的CSS和JavaScript,然后开始执行JavaScript代码。进入到项目的代码逻辑中,可以看到Vue中通过vue-router计算出当前路由匹配的组件,并且把这些组件显示到页面中,这样我们的页面就完全显示出来了。而我们性能优化的主要目的,就是让页面显示过程的时间再缩短一些。 ## 性能优化 从用户输入URL到页面显示的过程这个问题,包含着项目页面的执行流程。这个问题之所以重要,是因为我们只有知道了在这个过程中,每一步都发生了什么,之后才能针对每一步去做网络请求的优化,这也是性能优化必备的基础知识。 ### 网络请求优化 对于前端来说,可以优化的点,首先就是在首页的标签中,使用标签去通知浏览器对页面中出现的其他域名去做DNS的预解析,比如页面中的图片通常都是放置在独立的CDN域名下,这样页面加载首页的时候就能预先解析域名并把结果缓存起来 。 因为极客时间首页没做这个优化,所以我们以淘宝网的首页为例进行分析。你可以在[淘宝的首页源码](view-source:https://www.taobao.com)中看到下图所示的一列dns-prefetch标签,这样首页再出现img.alicdn.com这个域名请求的时候,浏览器就可以从缓存中直接获取对应的IP地址。 ![图片](https://static001.geekbang.org/resource/image/23/e6/23163cccae366e93afbe0125c77838e6.png?wh=1330x524) 项目在整体流程中,会通过HTTP请求加载很多的CSS、JavaScript,以及图片等静态资源。为了让这些文件在网络加载中更快,我们可以从后面这几方面入手进行优化。 首先,浏览器在获取网络文件时,需要通过HTTP请求,HTTP协议底层的TCP协议每次创建链接的时候,都需要三次握手,而三次握手会造成额外的网络损耗。如果浏览器需要获取的文件较多,那就会因为三次握手次数过多,而带来过多网络损耗的问题。 所以,首先我们需要的是让文件尽可能地少,这就诞生出一些常见的优化策略,比如先给文件打包,之后再上线;使用CSS雪碧图来进行图片打包等等。文件打包这条策略在HTTP2全面普及之前还是有效的,但是在HTTP2普及之后,多路复用可以优化三次握手带来的网络损耗。关于HTTP2的更多内容,你可以去搜索相关文章自行学习。 其次,**除了让文件尽可能少,我们还可以想办法让这些文件尽可能地小一些**,因为如果能减少文件的体积,那文件的加载速度自然也就会变快。这一环节也诞生出一些性能优化策略,比如CSS和JavaScript代码会在上线之前进行压缩;在图片格式的选择上,对于大部分图片来说,需要使用JPG格式,精细度要求高的图片才使用PNG格式;优先使用WebP等等。也就是说,尽可能在同等像素下,选择体积更小的图片格式。 在性能优化中,懒加载的方式也被广泛使用。图片懒加载的意思是,我们可以动态计算图片的位置,只需要正常加载首屏出现的图片,其他暂时没出现的图片只显示一个占位符,等到页面滚动到对应图片位置的时候,再去加载完整图片。 除了图片,项目中也会做路由懒加载,现在项目打包后,所有路由的代码都在首页一起加载。但是,我们也可以把不常用的路由单独打包,在用户访问到这个路由的时候再去加载代码。下面的代码中,vue-router也提供了懒加载的使用方式,只有用户访问了/course/:id这个页面后,对应页面的代码才会加载执行。 ```javascript { path: '/course/:id', component: () => import('../pages/courseInfo'), }, ``` 在文件大小的问题上,Lighthouse已经给了我们比较详细的优化方法,比如控制图片大小、减少冗余代码等等,我们可以在项目打包的时候,使用可视化的插件来查看包大小的分布。 我们来到项目根目录下,通过执行npm install操作来安装插件rollup-plugin-visualizer。使用这个插件后,我们就可以获取到代码文件大小的报告了。之后,进入到vite.config.js这个文件中,新增下列代码,就可以在Vite中加载可视化分析插件。 ```javascript import { visualizer } from 'rollup-plugin-visualizer' export default defineConfig({ plugins: [vue(),vueJsx(), visualizer()], }) ``` 然后,我们在项目的根目录下执行 npm run build命令后,项目就把项目代码打包在根目录的dist目录下,并且根目录下多了一个文件stat.html。 我们用浏览器打开这个stat文件,就能看到下面的示意图。项目中的ECharts和Element3的体积远远大于项目代码的体积,这时候我们就需要用懒加载和按需加载的方式,去优化项目整体的体积。 ![图片](https://static001.geekbang.org/resource/image/de/89/de38f42fb62c74yy9eb964fd399f7d89.png?wh=1920x1109) 那么这些文件如何才能高效复用呢?**我们需要****做的,就是****尽可能高效****地****利用浏览器的缓存机制,在文件内容没有发生变化的时候,做到一次加载多次使用**,项目中如果成功复用一个几百KB的文件,对于性能优化来说是一个巨大的提升。 浏览器的缓存机制有好几个Headers可以实现,Expires、Cache-control,last-modify、etag这些缓存相关的Header可以让浏览器高效地利用文件缓存。我们需要做的是,只有当文件的内容修改了,我们才会重新加载文件。这也是为什么我们的项目执行npm run build命令之后,静态资源都会带上一串Hash值,因为这样确保了只有文件内容发生变化的时候,文件名才会发生变化,其他情况都会复用缓存。 ### 代码效率优化 在浏览器加载网络请求结束后,页面开始执行JavaScript,因为Vue已经对项目做了很多内部的优化,所以在代码层面,我们需要做的优化并不多。很多Vue 2中的性能优化策略,在Vue 3时代已经不需要了,我们需要做的就是**遵循Vue官方的最佳实践**,其余的交给Vue自身来优化就可以了。 比如computed内置有缓存机制,比使用watch函数好一些;组件里也优先使用template去激活Vue内置的静态标记,也就是能够对代码执行效率进行优化;v-for循环渲染一定要有key,从而能够在虚拟DOM计算Diff的时候更高效复用标签等等。然后就是JavaScript本身的性能优化,或者说某些实现场景算法的选择了,这里需要具体问题具体分析,在通过性能监测工具发现代码运行的瓶颈后,我们依次对耗时过长的函数进行优化即可。 我们来到src/App.vue文件中,看下面的代码,我们实现了一个斐波那契数列,也就是说,在我们实现的这个数列中,每一个数的值是前面两个数的值之和。我们使用简单的递归算法实现斐波那契数列后,在页面显示计算结果。 ```javascript function fib(n){ if(n<=1) return 1 return fib(n-1)+fib(n-2) } let count = ref(fib(38)) ``` 上面的代码在功能上,虽然实现了斐波那契数列的要求,但是我们能够感觉到页面有些卡顿,所以我们来对页面的性能做一下检测。 我们打开调试窗口中的Performance面板,使用录制功能后,便可得到下面的火焰图。通过这个火焰图,我们可以清晰地定位出这个项目中,整体而言耗时最长的fib函数,并且我们能看到这个函数被递归执行了无数次。到这里,我们不难意识到这段代码有性能问题。不过,定位到问题出现的地方之后,代码性能的优化就变得方向明确了。 ![图片](https://static001.geekbang.org/resource/image/12/b9/12d7d30ea1f7bae6435fb6d5a21a80b9.png?wh=1920x1120) 下面的代码中,我们使用递推的方式优化了斐波那契数列的计算过程,页面也变得流畅起来,这样优化就算完成了。其实对于斐波那契数列的计算而言,得到最好性能的方式是使用数学公式+矩阵来计算。不过在项目瓶颈到来之前,我们采用下面的算法已经足够了,**这也是性能优化另外一个重要原则,那就是不要过度优化****。** ```javascript function fib(n){ let arr = [1,1] let i = 2 while(i<=n){ arr[i] = arr[i-1]+arr[i-2] i++ } return arr[n] } ``` ## 用户体验优化 性能优化的主要目的,还是为了能让用户在浏览网页的时候感觉更舒服,所有有些场景我们不能只考虑单纯的性能指标,还要结合用户的交互体验进行设计,**必要的时候,我们可以损失一些性能去换取交互体验的提升。** 比如用户加载大量图片的同时,如果本身图片清晰度较高,那直接加载的话,页面会有很多图一直是白框。所以我们也可以预先解析出图片的一个模糊版本,加载图片的时候,先加载这个模糊的图作为占位符,然后再去加载清晰的版本。虽然额外加载了图片文件,但是用户在体验上得到了提升。 类似的场景还有很多,比如用户上传文件的时候,如果文件过大,那么上传可能就会很耗时。而且一旦上传的过程中发生了网络中断,那上传就前功尽弃了。 为了提高用户的体验,我们可以选择断点续传,也就是把文件切分成小块后,挨个上传。这样即使中间上传中断,但下次再上传时,只上传缺失的那些部分就可以了。可以看到,断点上传虽然在性能上,会造成网络请求变多的问题,但也极大地提高了用户上传的体验。 还有很多组件库也会提供骨架图的组件,能够在页面还没有解析完成之前,先渲染一个页面的骨架和loading的状态,这样用户在页面加载的等待期就不至于一直白屏,下图所示就是antd-vue组件库骨架图渲染的结果。 ![图片](https://static001.geekbang.org/resource/image/26/dc/26b97a7c7ba894d18ac6311a7fd966dc.gif?wh=872x472) ## 性能监测报告 在[第12讲](https://time.geekbang.org/column/article/442479)学习Vue Devtools的时候,我们已经使用Chrome的性能监测工具Lighthouse对极客时间的官网做了一次性能的评估,我们可以在这里看到[评测报告](https://pandafe.gitee.io/clock/time.geekbang.org.html)。并且,我们也对如何在调试窗口的Performance页面中进行性能监控,给出了演示。为了方便你理解,我们在这里也解释一下FCP、TTI和LCP这几个关键指标的含义。 首先是First Contentful Paint,通常简写为FCP,它表示的是页面上呈现第一个DOM元素的时间。在此之前,页面都是白屏的状态;然后是Time to interactive,通常简写为TTI,也就是页面可以开始交互的时间;还有和用户体验相关的Largest Contentful Paint,通常简写为LCP,这是页面视口上最大的图片或者文本块渲染的时间,在这个时间,用户能看到渲染基本完成后的首页,这也是用户体验里非常重要的一个指标。 我们还可以通过代码中的performance对象去动态获取性能指标数据,并且统一发送给后端,实现网页性能的监控。性能监控也是大型项目必备的监控系统之一,可以获取到用户电脑上项目运行的状态。 下图展示了performance中所有的性能指标,我们可以通过这些指标计算出需要统计的性能结果。 ![图片](https://static001.geekbang.org/resource/image/71/f1/71a25ac4634b288911f17beb97b429f1.png?wh=912x555) ```plain let timing = window.performance && window.performance.timing let navigation = window.performance && window.performance.navigation DNS 解析: let dns = timing.domainLookupEnd - timing.domainLookupStart 总体网络交互耗时: let network = timing.responseEnd - timing.navigationStart 渲染处理: let processing = (timing.domComplete || timing.domLoading) - timing.domLoading 可交互: let active = timing.domInteractive - timing.navigationStart ``` 在上面的代码中,我们通过Performance API获取了DNS解析、网络、渲染和可交互的时间消耗。有了这些指标后,我们可以随时对用户端的性能进行检测,做到提前发现问题,提高项目的稳定性。 ## 总结 今天的主要内容就聊完啦,我们来复习一下今天学到的内容吧。 首先我们了解了用户从输入URL到页面显示的这一过程发生了什么,这里面的每个流程都有值得优化的地方,比如网络请求、页面渲染等。在对这些流程优化后,网页运行时整体的性能都会得到提升。 之后,在网络请求优化这一部分,我们首先谈到,对于DNS,我们可以通过dns-prefetch预先获取,这对性能优化来说,会减少页面中其他域名请求的DNS解析时间;因为TCP协议每次链接时,都需要三次握手,而这会带来额外的网络消耗的问题,为了解决这一问题,我们的优化策略是让文件尽可能少一些,并且也小一些。 比如,我们可以通过文件打包的形式减少HTTP请求数量,这样对于文件的大小来说,可以减小文件体积。我们也可以压缩代码,以及选择更合适的图片格式,这些都可以让我们加载更小的文件。图片的懒加载和路由的懒加载可以让首页加载更少的文件,从而实现页面整体性能的优化。 在讲完网络请求优化后,我们又研究了代码效率优化这个问题,其实代码层面要做的优化并不多,主要还是遵守Vue 3最佳实践。我们还以斐波那契数列的计算为例,通过在Performance面板中进行性能监控,明确了代码优化的方向。在通过递归的方式优化斐波那契数列之后,我们能明白这样一点:**性能优化的一个重要原则,是不要****过度****优化**。 之后,在用户体验优化这一部分,我们的关注点是在交互体验的优化上。有些场景我们可以损失部分性能去换取体验的提升,比如通过骨架图,我们可以在页面加载之前,通过对图片预先加载出模糊版本,可以让用户获得更好的体验。 最后,在性能监测报告这一部分,我讲到选择合适的工具,可以帮助我们实时地监测项目的性能。我们通过Lighthouse性能报告和Performace监测工具,可以精确地定位到项目瓶颈所在,有针对地去进行性能优化。 ## 思考题 最后,给你留一个思考题:通过今天的学习,想一想你负责的项目都有哪些可以优化的地方呢? 欢迎在留言区留言分享你的想法,也欢迎你把这一讲的内容推荐给你的朋友、同事们,我们下一讲见。