ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Vue首屏加载优化:从vendor.js到路由懒加载与CDN加速

2026/10/1 20:32:23 拓冰建站 浏览量
Vue首屏加载优化:从vendor.js到路由懒加载与CDN加速 接手过一个Vue 2后台管理系统打包完的vendor.js接近6MB路由也没有做拆包生产环境首次访问的白屏时间稳稳超过5秒。当时我打开控制台看Network面板光JS文件就有十几个请求一个接着一个排队下载首页要用的、不要用的代码全部混在几个大chunk里。说实话那种体验放在今天用户基本不会给你第二次机会。这不是个例只要Vue项目做到一定规模首次访问加载慢几乎是必然要踩的坑。Vue首屏加载慢本质上是“资源体积大”和“请求链路长”叠加的结果。浏览器要先下载HTML然后解析出CSS和JS的引用再逐个下载、执行执行完JS才有可能把首屏渲染出来。任何一环变慢白屏时间都会被拉长。这篇内容我就顺着这个逻辑往下拆从定位问题、打包优化、传输链路优化到地图、播放器这类重型依赖的特殊处理方式最后把常见的翻车案例也整理出来。无论你用的是Vue 2加Webpack还是Vue 3加Vite思路基本通用配置上我尽量两边都写出示例。顺便提一句面试官喜欢问的“Vue首屏优化怎么做”答案基本都藏在这篇文章里了。1. 先搞清楚首屏为什么会慢1.1 三个最直接的原因包太大、请求太多、执行太久先看最容易理解的一个原因构建产物体积过大。项目在开发环境跑得飞快是因为Webpack Dev Server或Vite Dev Server做了按需编译浏览器只加载当前页面用到的模块。但生产构建不一样如果没有特别注意webpack会把几十个页面、几十个组件、一大票第三方库打成一个或几个大文件。一个2MB的vendor.js在4G网络下下载就要好几秒更别提下载完还要交给JS引擎去解析执行。然后是请求数量过多且串行。浏览器对同一域名的并发请求数是有限制的HTTP/1.1时代一般默认6个左右。如果首屏有30个资源要加载那么就要分好几批排队。每多一个资源就多一次DNS查询、TCP建连、HTTP请求往返的开销。这个问题在HTTP/2环境下会好一些但很多内部系统到今天仍然跑的是HTTP/1.1。最后是JS执行阻塞渲染。浏览器解析HTML时遇到script标签会暂停HTML解析先下载并执行JS。也就是说JS文件越大、逻辑越复杂白屏时间就越长。很多人把“下载完成”当成页面快要显示了其实下载完只是开始JS解析、编译、执行完DOM才能被真正画出来。1.2 先判断是前端资源慢还是接口响应慢做优化前一定要先定位瓶颈不要一上来就改打包配置。打开Chrome DevTools的Network面板刷新页面看瀑布图Waterfall里最耗时的条目。如果花费时间集中在JS、CSS这类静态资源上比如某个vendor.js下载花了3秒那问题在前端资源。如果静态资源下载很快但某个查询接口从发起请求到拿到响应花了2秒那说明瓶颈在后端或网络链路。还有一种情况很典型接口本身不慢但它发起的时间很晚因为是在JS执行完、组件挂载之后才调用的。这种情况属于前端执行逻辑拖慢了请求时机。我见过不少项目前端把路由懒加载、gzip、CDN全做了首屏还是慢最后查出来是后端一个列表查询接口要跑3秒。所以排查顺序建议是先看Network瀑布图把耗时大户找出来再决定从哪一层动手。1.3 用指标说话别凭感觉判断白屏时间很多人说的“首屏加载慢”其实凭的是感觉。真正要量化建议关注这几个浏览器性能指标FPFirst Paint第一次绘制像素到屏幕的时间。FCPFirst Contentful Paint第一次绘制出文本、图片等内容的快照时间。这更接近用户感知的“白屏结束”。LCPLargest Contentful Paint页面最大内容绘制完成的时间通常指首屏主体内容出现。TTITime to Interactive页面可以稳定响应用户交互的时间。在真实用户环境下最简单的方式是用Chrome DevTools的Lighthouse跑一次看Performance分数和FCP、LCP的具体数值。开发环境想快速看也可以直接在控制台执行window.addEventListener(load, () { const paintEntries performance.getEntriesByType(paint) const fcp paintEntries.find(e e.name first-contentful-paint) console.log(FCP:, fcp ? fcp.startTime : N/A) })有了指标之后每次改动都能做前后对比优化的效果也就有了依据后面验证成果的时候会省很多事。2. 打包阶段的优化动作2.1 路由懒加载是性价比最高的第一步所谓路由懒加载就是让每个路由页面在访问时才去加载对应的JS代码而不是项目启动时把所有页面代码全部下载下来。对于Vue Router实现方式非常简单// 原来是这样的import Home from /views/Home.vue // 改成这样 const Home () import(/* webpackChunkName: home */ /views/Home.vue) const routes [ { path: /, name: Home, component: Home } ]这样webpack在构建时会把Home组件单独拆分到一个chunk文件里。用户首次访问/时只会下载home.js这个chunk而不会把用户中心、订单列表、详情页的代码一次性拉下来。动态import()语法不只适用于路由也适用于大组件。假设你的首页不需要富文本编辑器那就不应该在入口文件里直接import它而是应该用Vue 3的defineAsyncComponentimport { defineAsyncComponent } from vue const RichTextEditor defineAsyncComponent(() import(/components/RichTextEditor.vue))拆包粒度需要注意不要拆得过碎。我见过有人把每个弹窗组件都做成动态import结果一个页面的chunk数量动辄几十个HTTP/1.1下反而更慢。合理的粒度是一级路由页面按页面拆业务模块内再按需要拆只有体积大、使用频率低的组件才考虑单独拆包。如果要用prefetch预加载下一个页面建议只对“用户很有可能点击”的下一级页面加webpackPrefetch: true不要全局给所有动态路由都加上否则首屏空闲时浏览器会把所有chunk都预加载效果适得其反。2.2 第三方库按需引入别让UI库背锅很多Vue项目体积膨胀罪魁祸首是UI组件库被全量引入了。早期的Element UI、Ant Design Vue全量引入体积都不小。如果只用到了其中的十几个组件完全没必要把整库都打包进去。以Element Plus为例推荐用官方生态的按需引入方案// vite.config.ts import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })如果你用的是Vue CLI / Webpack配置原理一样只需要把unplugin-auto-import和unplugin-vue-components的Webpack版本接入configureWebpack.plugins即可。这样项目中用到的组件和对应的样式会自动按需引入没有用到的组件不会打包进去。除了UI库还有几个常见的大体积依赖需要特别留意moment.js体积大且已停止维护建议用dayjs替代API几乎一致体积差一个量级。lodash建议使用lodash-es借助ES Modules的tree-shaking只打包用到的函数。ECharts支持按需注册模块不要直接import * as echarts from echarts改成只引入用到的图表和组件。如果你想进一步把稳定的第三方库单独拆出来利用浏览器缓存可以用webpack的splitChunks配置。思路是把vue、vue-router、pinia、axios这类不经常变化的包放进独立的vendor chunk这样它们可以单独做长缓存业务代码更新时不会导致vendor文件重新下载。configureWebpack: { optimization: { splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: vendors, chunks: all } } } } }不过splitChunks的配置非常容易过度设计拆得不好反而会产生重复代码、缓存失效。我的建议是新手阶段保持简单先只拆一个vendor等到对包体变化有感知了再针对性地调优。2.3 构建期生成gzip文件文本类资源经过gzip压缩后体积能下降60%到75%。一个2MB的JS文件压缩后可能只有700KB下载时间直接减半。启用gzip有两种常见方式第一种是纯服务器运行时压缩比如Nginx里开启gzip on;服务器实时压缩后返回给浏览器。好处是配置简单缺点是每次请求都会消耗一些CPU做压缩。第二种是构建期生成.gz文件服务器直接返回预压缩好的文件省去实时压缩的CPU开销。这才是生产环境推荐的做法。Webpack项目用compression-webpack-pluginconst CompressionWebpackPlugin require(compression-webpack-plugin) configureWebpack: { plugins: [ new CompressionWebpackPlugin({ algorithm: gzip, test: /\.(js|css|html|svg)$/, threshold: 10240, minRatio: 0.8 }) ] }Vite项目则用vite-plugin-compression配置思路差不多。生成.gz文件后Nginx需要开启gzip_static on;来支持读取预压缩文件gzip on; gzip_static on; gzip_vary on; gzip_min_length 1k; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript text/javascript application/xml image/svgxml;需要注意gzip_comp_level并不是越大越好5到6已经能兼顾压缩率和CPU开销开到9不仅压缩时间变长体积也不会再明显减少。另外如果你用了CDN要确保CDN回源时源站开启了gzip或CDN配置了压缩。否则用户拿到的是未压缩的文件前面做的一切都白费。3. 传输链路优化CDN、缓存与连接3.1 把核心库外置到CDN给vendor.js减负打包优化的终点往往是“能不打包就不打包”。对于vue、vue-router、axios这类不经常升级的库可以通过externals配置把它们从打包产物中剔除然后在index.html里用script标签从CDN引入。这样既能减少app.js和vendor.js的体积又能利用CDN的边缘节点加速访问。Webpack项目的配置configureWebpack: { externals: { vue: Vue, vue-router: VueRouter, axios: axios, pinia: Pinia } }对应的index.html里需要手动引入CDN资源并且必须放在app.js之前script srchttps://cdn.example.com/vue3.4.21/dist/vue.global.prod.js/script script srchttps://cdn.example.com/vue-router4.3.0/dist/vue-router.global.prod.js/script script src/js/app.js/script这里有一个版本匹配的坑必须强调Vue 3项目必须用配套的vue-router 4Vue 2项目必须用vue-router 3混用会直接导致页面白屏。如果项目部署在内网或隔离环境外网CDN可能完全加载不出来这时候就需要使用公司自建CDN或者在index.html里加一个兜底逻辑检测CDN资源加载失败后动态改为加载本地静态文件。script if (!window.Vue) { document.writeln(script src/static/vendor/vue.global.prod.js\/script); } /scriptexternals确实能显著减小包体但我建议只外置核心运行时框架和工具库不要把所有依赖都外置。外置依赖太多会带来版本管理混乱的问题而且部分国内CDN资源更新不及时反而引入安全风险。3.2 Nginx缓存策略配合hash文件名Vue构建出来的文件名默认带了hash值比如app.a1b2c3.js。hash的意义在于文件内容变了文件名就变了文件名变了浏览器就会重新下载。也就是说静态资源可以放心地设置长期缓存而不用担心用户拿到旧版本。Nginx下推荐这样配置server { listen 80; server_name example.com; root /usr/share/nginx/html/dist; index index.html; # 带hash的静态资源长缓存 location ~* \.(?:js|css|png|jpg|jpeg|gif|webp|svg|woff2?)$ { expires 30d; add_header Cache-Control public, no-transform; } # index.html 不缓存保证发布后用户能拿到最新引用 location /index.html { add_header Cache-Control no-cache, no-store, must-revalidate; } # 前端路由 history 模式 location / { try_files $uri $uri/ /index.html; } }很多团队在缓存上踩坑原因是index.html也被设置了长缓存。index.html是前端资源的入口它引用哪个JS、CSS文件全靠它的内容。一旦它被浏览器缓存住用户下次访问时看到的还是旧文件的引用即使服务端已经发布了新版本也只能等缓存过期。3.3 用preconnect和dns-prefetch提前建立连接这个优化很小但几乎零成本。当HTML里引用了CDN资源和后端API域名时浏览器解析到对应标签才能开始DNS查询和建立连接。如果提前告诉浏览器“我待会要访问这些域名”就能节省几百毫秒。在index.html的head里加link reldns-prefetch href//cdn.example.com / link relpreconnect href//api.example.com / link relpreconnect href//cdn.example.com crossorigin /dns-prefetch负责提前做DNS解析preconnect更激进会提前建立TCP连接如果目标资源启用了HTTPS还会提前做TLS握手。需要注意preconnect只适合少量关键域名用多了会占用浏览器的连接名额反而不利。4. 重型依赖场景的特化处理4.1 “vue播放m3u8”这类视频场景别在入口加载播放器最近很多人搜“vue播放m3u8”说明视频播放场景在Vue项目里越来越常见。m3u8直播流通常用hls.js来播放流媒体文件本身不是问题真正拖慢首屏的是hls.js这个库。它的大小在几百KB左右如果直接在入口文件里import Hls from hls.js那所有访问首页的用户都要为“可能根本没打开的视频功能”买单。正确的做法是把播放器相关逻辑独立到一个组件里在播放页面内部动态加载const VideoPlayer () import(/components/VideoPlayer.vue)然后在需要初始化播放器的逻辑里再异步加载hls.jsasync function initPlayer(videoEl, url) { const { default: Hls } await import(hls.js) if (Hls.isSupported()) { const hls new Hls() hls.loadSource(url) hls.attachMedia(videoEl) } }如果是列表页里有大量视频预览还可以考虑延迟到用户点击播放时才初始化播放器。之前有个项目就是这个优化首屏体积直接减掉400多KB白屏时间缩短了将近1秒。4.2 地图SDK要进入地图页面才加载另一个常见的大依赖是地图SDK。腾讯地图、高德地图的JS SDK通常都是通过动态加载script标签引入的体积大且加载慢。如果项目在登录后立刻加载地图SDK首屏会被拖得很惨。推荐的做法是封装一个动态加载函数只有进入地图相关页面时才发起加载function loadScript(src, globalName) { return new Promise((resolve, reject) { if (window[globalName]) { resolve(window[globalName]) return } const script document.createElement(script) script.src src script.onload () resolve(window[globalName]) script.onerror reject document.head.appendChild(script) }) } async function openMapPage() { const AMap await loadScript(https://webapi.amap.com/maps?v2.0keyYOUR_KEY, AMap) // 初始化地图实例 }同样如果你用的是vue-amap、vue-baidu-map这类封装组件也要注意按需启用相关组件不要全局一次性use全部插件。可以把地图功能独立成路由懒加载的页面用户在地址列表点击“查看地图”后再请求加载SDK用户基本无感知。4.3 UI组件库与图表库容易被人忽略的全局注册问题很多项目都会在main.js里这样写app.use(ElementPlus) app.use(VueEcharts)如果只用了少量组件这显然不划算。UI组件库的按需引入在2.2里已经介绍过这里想强调的是图表库。ECharts按需注册的写法是这样的import * as echarts from echarts/core import { BarChart, LineChart, PieChart } from echarts/charts import { GridComponent, TooltipComponent, LegendComponent } from echarts/components import { CanvasRenderer } from echarts/renderers echarts.use([BarChart, LineChart, PieChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer])ECharts的按需引入能把体积从800多KB降到200KB甚至更低对于数据分析类页面效果立竿见影。5. 优化后的通用排查与操练5.1 从“看到文件”到“看到数字”的排查流程如果你接手一个已经“病入膏肓”的项目请不要急着改代码。先按下面的流程走一遍把事实摸清楚打开Network面板刷新页面按资源大小排序找出体积最大的Top 5。看瀑布图找出耗时最长的请求判断是下载慢还是等待响应慢。用构建分析工具看打包内容。Webpack项目用webpack-bundle-analyzerVite项目用rollup-plugin-visualizer。跑一次构建浏览器会自动打开可视化分析页面你能直观看到每个依赖占了多少体积。记录优化前的FCP、LCP和总资源体积。根据优先级逐项优化每做一步都量一次保留记录。这里特别强调一下不要一次性把多个优化同时做完然后只看最终结果因为你很难知道是哪一步产生了效果哪些步骤其实没有意义。我见过有人同时改了路由懒加载、CDN外置、splitChunks结果首屏反而更慢了排查了半天才发现是CDN资源加载失败每个script标签都在等超时。5.2 优化优先级参考从最稳的开始我按照实施难度和收益大概整理了一个优先级优先级优化动作预期效果主要风险P0开启gzip压缩总体积下降60%以上见效极快需要确认服务端或CDN支持P0路由懒加载首屏只加载当前页面代码拆包粒度控制不好会碎片化P1公共库外置到CDNvendor体积显著减小CDN不可用、版本不匹配P1第三方库按需引入解决UI库、图表库体积过大需要逐个排查依赖P2图片压缩/懒加载减少图片请求耗时懒加载占位图可能影响体验P2接口并行请求减少白屏等待时间需要处理好错误边界P2HTTP/2并发多个小请求依赖运维环境支持建议按P0到P2的顺序推进避免一上来就做风险较高的改动。前四项做完正常情况下首屏耗时已经能有很明显的改善。5.3 经典翻车案例踩过一遍再也不想踩案例一路由懒加载后页面首次点击直接白屏出现这个问题的项目排查后发现是publicPath配置不正确。当publicPath使用了相对路径某个chunk在实际加载时会前往错误的路径请求文件。解决办法是把publicPath设为绝对路径一般用/即可output: { publicPath: / }如果你的项目部署在子路径下要相应地设置为子路径比如/admin/。案例二gzip文件生成了但响应头没有Content-Encoding: gzip检查了服务器配置发现gzip_static on;确实开了但用户请求仍然没有返回gzip。最后原因很常见资源请求被CDN拦截而CDN的缓存里存的是未压缩的原始文件。处理办法是在CDN控制台开启源站压缩跟随或者让CDN直接配置gzip压缩。案例三加了webpackPrefetch后首屏请求数暴增动态import的魔法注释webpackPrefetch: true本意是提前加载下一个页面但如果所有路由都加了浏览器会在空闲时把全部chunk都预取一遍。使用低配手机和弱网环境时这会让首屏完全失控。解决办法很简单去掉全局的prefetch只给真正“下一屏”的页面加。案例四第三方库外置后页面报错Vue is not definedexternals配置没问题index.html里的CDN标签也加了但用户就是白屏。排查发现是CDN脚本加载失败但业务代码没有做任何兜底处理。后来加了检测window.Vue是否存在的fallback逻辑问题解决。这个案例提醒我们外置CDN一定要考虑可用性不要只图包体变小。最后说一点实际的体会做Vue首屏优化最忌讳的是“所有优化手段都上”。我见过有人把gzip、CDN、路由懒加载、图片压缩、HTTP/2全做了结果首屏反而比之前还慢原因可能是CDN回源太慢、拆包过碎、prefetch抢占了关键资源带宽。优化这件事更像是在约束条件下做取舍而不是堆叠技术名词。我个人比较推荐的做法是固定一个指标比如FCP或者“从输入URL到首屏可交互的时间”每次改完配置都在开发环境build一次用Lighthouse或Performance面板量一次。这样你能清楚地知道每改动一个变量数据产生了什么变化长期积累下来对自己项目的瓶颈点会形成很准确的直觉。再分享一个小技巧可以在main.js里临时加一段代码把关键资源请求耗时打印在开发者工具控制台观察路由懒加载后各个chunk是谁、什么时候被触发的。这样排查首屏慢时能快速看出是不是某个意外引用的组件把大库拉进来了。优化首屏是一个反复“测量-调整-验证”的过程不是写两行配置就结束的事情。希望这篇内容能帮你少走一些弯路把你的Vue项目从慢到能用、从能用到好用。