Vue首屏优化指南:4种骨架屏实现方案,告别白屏
发布时间:2026/10/1 4:39:26来源:尧图网络
跟UI撕需求、跟后端对接口、跟网络环境死磕每一个做Vue首屏优化的前端都会遇到同一个问题白屏。用户打开页面地址栏已经变了但页面空空如也转圈、闪烁、卡顿这些体验往往不是功能问题而是首屏渲染那段看不见的时间里页面没有东西可看。骨架屏就是用来填这个空档的。今天这篇我把日常项目里真正落地过的4种Vue骨架屏方案完整拆一遍从原理到实操代码从踩坑记录到选型建议尽量让想做的朋友能直接参考。先说清楚一点骨架屏不是加载动画它是在数据还没到达、Vue实例还没渲染完的时候先用静态的灰块/线条把页面的大致结构画给用户看。用户知道页面正在加载、知道大概会看到什么内容焦虑感会明显下降。它对首屏体验的提升最直观的体现在两个指标上FCPFirst Contentful Paint和LCPLargest Contentful Paint。FCP是从白屏到出现第一个内容的耗时骨架屏本身就是第一个内容所以它能快速拉高FCPLCP反映的是最大内容块的加载时间骨架屏把主要区域占位出来后用户感知的加载速度也会快很多。说白了骨架屏是用看起来快去弥补实际上的加载等待时间这是体验层面性价比非常高的手段。这篇文章适合三类人看一是被首屏加载速度折磨、想系统性地做一次体检和改造的前端开发二是刚接触Vue不久、对骨架屏的几种写法还停留在听说过但没用过阶段的新手三是准备在团队里定一个统一的首屏优化方案、需要对比不同实现路线的技术负责人。下面我按从最省力到最可控的顺序把4种方案逐一展开。1. 从白屏到骨架屏先搞懂首屏到底慢在哪1.1 白屏时间是怎么来的不讲清楚问题根源就去上骨架屏大概率是白忙活。首屏白屏的时间不是某一个环节单独决定的而是一整条链路的累积DNS解析建立连接域名有几次解析、TLS握手要几轮取决于网络环境和CDN配置。下载HTML如果index.html里塞了一大堆内联脚本和样式这一步就会很慢。下载JS/CSS打包产物体积越大、没有做分包和压缩下载时间越长。很多Vue项目的首屏慢八成是vendor.js几MB起步直接拖垮了下载速度。解析执行JS浏览器拿到JS后要parse、compile、executeVue的运行时、路由、状态管理、第三方组件库都在这个阶段参与。挂载渲染把Vue实例挂载到#app上走完组件的created、mounted生命周期真正的DOM才会出现。如果上面每一环都慢一点最终的白屏可能就是3秒、5秒甚至更久。骨架屏能优化的部分是把挂载渲染之前的等待时间填上内容。它不能帮你减少JS体积不能帮你加速网络请求但它能让用户的体验从一片空白变成页面结构已经出来了。提示上骨架屏之前先用Chrome DevTools的Performance面板录制一次页面加载把FCP、LCP、DCL、Load这些指标记下来。改造之后要有对比数据否则你都不知道优化到底有没有效果。1.2 骨架屏不是loading动画别搞混了骨架屏和loading动画很容易被混为一谈但两个东西的定位完全不同。loading动画是一个笼统的转圈圈它告诉用户在加载请等待但它没有告知页面大概长什么样骨架屏模拟的是真实页面的布局结构用户一看就知道这里是标题、这里是列表、这里是图表区域感知上会更接近页面即将呈现的样子。从实现角度来说骨架屏通常以静态HTMLCSS的形式存在在真实组件渲染之前就被浏览器绘制出来了所以它对JS依赖很小loading动画则经常是Vue组件的一部分Vue都还没挂载呢loading组件自己可能都还没渲染出来。这也是很多团队做了loading组件却发现首屏根本看不到它的原因——组件代码本身就在那个巨大的JS bundle里。理解了这层区别你就明白为什么有些骨架屏方案要把静态结构直接写进index.html而不是写在Vue组件里了。下面这4种方案按是否依赖Vue运行时和接入成本两个维度划分正好覆盖了所有典型场景。2. 方案一Element Plus骨架屏组件5分钟接入2.1 组件级骨架屏的完整代码如果你项目里已经用了Element Plus最省事的方案就是用它的el-skeleton组件。这个方案不需要额外安装任何东西也不需要改构建配置直接在组件内部根据数据状态切换骨架屏和真实内容即可。template div classproduct-page !-- 数据加载中显示骨架屏 -- el-skeleton v-ifloading animated template #template el-skeleton-item variantimage stylewidth: 100%; height: 320px / div stylepadding: 16px el-skeleton-item varianth1 stylewidth: 60% / el-skeleton-item varianttext stylemargin-top: 16px / el-skeleton-item varianttext stylewidth: 80% / /div /template /el-skeleton !-- 数据到达后渲染真实内容 -- div v-else img :srcproduct.image alt商品图 / h1{{ product.name }}/h1 p{{ product.description }}/p /div /div /template script setup import { ref, onMounted } from vue import { getProductDetail } from /api const loading ref(true) const product ref({}) onMounted(async () { try { product.value await getProductDetail() } finally { loading.value false } }) /script这段代码的逻辑很简单请求发出时loading为true展示骨架屏接口返回后切换为false真实DOM替换掉骨架屏。Element Plus自带animated属性的脉冲光效视觉上比纯灰块活泼很多不需要自己写动画。2.2 组件级骨架屏的适用边界el-skeleton这类组件级方案我最推荐用在页面内某个数据区块的加载场景。典型的有三类表格数据加载、详情页的正文区域、仪表盘/后台的卡片区块。这些场景的特点是页面其他部分已经渲染出来了只有某个模块在等接口给这个模块单独加一个骨架屏体验提升非常直观。但这个方案的短板也很明确它解决不了整个页面第一次加载时的白屏。因为组件代码需要Vue挂载后才会执行而在JS下载完成之前连Vue都没有启动组件级的骨架屏自然显示不出来。换句话说如果用户从地址栏访问网速再慢一点JS还没执行的时候页面依然是一片空白——el-skeleton根本来不及出现。所以我把这个方案定位成局部优化的好手、首屏的助手。如果你现在的诉求是详情页接口慢用户等得烦躁用它是合适的如果诉求是整个应用首屏白屏3秒什么都没显示请继续往下看方案二和方案三。3. 方案二手写静态骨架屏把首屏HTML完全掌握在自己手里3.1 在index.html里直接写骨架屏手写骨架屏是最原始但最可靠的方案它的核心思路是在index.html的div idapp标签内部预置一份静态的骨架屏HTML结构。浏览器解析HTML的时候就能直接绘制完全不用等JS。等Vue实例挂载成功、执行了挂载后的回调再把这份静态骨架屏移除。以Vue CLI项目为例默认的public/index.html里是这样!DOCTYPE html html langzh-CN head meta charsetutf-8 / meta nameviewport contentwidthdevice-width, initial-scale1 / title电商详情页/title /head body div idapp/div script src/js/chunk-vendors.js/script script src/js/app.js/script /body /html改造后#app里面加入骨架屏结构!DOCTYPE html html langzh-CN head meta charsetutf-8 / meta nameviewport contentwidthdevice-width, initial-scale1 / title电商详情页/title style .skeleton { padding: 16px; background: #fff; } .skeleton .sk-block { border-radius: 8px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 37%, #f0f0f0 63%); background-size: 400% 100%; animation: sk-loading 1.4s ease infinite; } keyframes sk-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } } .skeleton .sk-title { width: 60%; height: 28px; } .skeleton .sk-line { width: 100%; height: 16px; margin-top: 12px; } .skeleton .sk-cover { width: 100%; height: 320px; border-radius: 12px; } /style /head body div idapp div classskeleton div classsk-block sk-cover/div div classsk-block sk-title stylemargin-top: 16px/div div classsk-block sk-line/div div classsk-block sk-line stylewidth: 80%/div div classsk-block sk-line stylewidth: 90%/div /div /div script src/js/chunk-vendors.js/script script src/js/app.js/script /body /html这段骨架屏的CSS用了linear-gradient移动的动画形成一道光扫过的效果视觉接近常见的加载中体验。关键在于这段HTML和CSS不依赖任何JS浏览器HTML解析到#app内部时就渲染出来了。index.html本身很小这个骨架屏结构叠加的内联CSS也只有几百字节不会对HTML下载造成明显负担。3.2 Vue实例挂载后如何优雅移除静态骨架屏是塞在#app里的Vue挂载的时候会把#app内部原有内容替换掉吗答案是会。Vue的挂载逻辑本身就是用虚拟DOM替换掉挂载点的innerHTML所以只要你把骨架屏放进#app内部正常挂载后它就会被清掉不需要额外处理——这是很多人没注意到的一个点。但在Vue 3里面有个细节需要注意。如果你用的是createApp(App).mount(#app)挂载过程会直接覆盖#app的子节点如果你把el-skeleton或者业务组件和静态骨架屏混在一起旧的骨架屏可能残留。更稳妥的做法是手动在onMounted里删除骨架屏DOMimport { createApp } from vue import App from ./App.vue createApp(App).mount(#app)按上面的写法Vue挂载时静态骨架屏HTML会被替换掉不会在DOM里留下残影。如果你确实不放心或者页面里有异步组件挂载比较慢可以在main.js里主动清理// main.js挂载完成后主动移除骨架屏 const app createApp(App) app.mount(#app)这里不需要写document.querySelector(.skeleton).remove()因为Vue的mount本身就会清空挂载点。真遇到了骨架屏没消失的情况优先检查是否是挂载选择器写错了、或者你用了v-if把真实内容包住了导致一直走不到挂载分支。3.3 手写骨架屏的两个关键细节第一个细节骨架屏的样式不能和业务CSS混淆。尽量以内联style形式写在head里并且骨架屏的class名要足够独特比如统一加sk-前缀避免被全局CSS污染也避免污染业务样式。很多项目用了scoped样式但骨架屏是纯静态HTML不在组件scope内所以它的样式必须独立存放。第二个细节骨架屏的结构要和真实页面保持大致一致但不追求完全一致。你不需要把每个像素都对齐核心是让用户感知这个区域是图、这个区域是标题、这个区域是几行文本。比例估算可以用真实的UI稿来匹配比如封面图高度占视口的40%、标题占内容区宽度的60%出来的效果就会比较自然。手写方案的优点是零依赖、可控性极强、SEO也友好——搜索引擎爬虫拿到的HTML里有真实的内容占位结构比纯空白的#app要好。缺点同样明显页面结构一变骨架屏也要手动跟着改多页面项目里每个页面都要维护一份骨架屏只能做静态结构做不了太复杂的交互。所以它最适合页面结构稳定、没有太多动态变化的项目。4. 方案三page-skeleton-webpack-plugin一键生成但别急着用4.1 生成器的工作方式如果你觉得手写骨架屏太麻烦想自动生成可以看看page-skeleton-webpack-plugin这个方案。这是饿了么团队开源的一个webpack插件它的工作流程让我当时第一次用的时候觉得挺神奇在开发模式启动一个专门的目标页面。插件拿Puppeteer去无头浏览器里打开这个页面。页面正常渲染出真实DOM后插件对DOM进行分析把关键节点替换成占位块灰色方块/线条生成对应的HTML和CSS。构建时把这段骨架屏HTML注入到index.html的#app中。也就是说你做了一次真实页面渲染它就帮你把骨架屏仿制出来不用手动去量每个块的高度宽度不用手写CSS动画。理论上它对复杂布局的还原度比手写高很多。4.2 接入vue.config.js的坑这个插件已经很久没更新了对Vue CLI 5和webpack 5的支持需要多试几个版本网上很多教程用的还是老配置。我实际接入时的做法是这样的npm install page-skeleton-webpack-plugin --save-dev然后单独创建一个生成骨架屏用的路由配置。官方推荐的做法是在项目里新增一个skeleton相关的入口页而不是直接在业务入口里跑。因为插件是通过访问页面来抓取骨架屏的访问的页面应该是只包含需要提取骨架屏的组件。// vue.config.js const SkeletonWebpackPlugin require(page-skeleton-webpack-plugin) const isSkeleton process.env.SKELETON true module.exports { configureWebpack: (config) { if (isSkeleton) { config.plugins.push( new SkeletonWebpackPlugin({ routes: [/], // 需要生成骨架屏的路由 minimize: true, // 压缩生成的CSS quiet: true, }) ) } }, }然后通过环境变量来区分当前是普通构建还是骨架屏生成{ scripts: { serve: vue-cli-service serve, build: vue-cli-service build, skeleton: SKELETONtrue vue-cli-service serve } }跑npm run skeleton后插件会在命令行交互里让你选择要生成骨架屏的页面数据然后自动完成上面的流程。注意这个插件对Vue 3的支持并不好目前你能搜到的大部分博客教程还是基于Vue 2 Vue CLI。如果你用的是Vue 3 Vite的技术栈不建议强行上这个方案推荐直接看方案四。4.3 生成后必须要做的三件事插件生成完骨架屏不等于直接能用我实际使用后总结出三件必须做的事第一检查骨架屏的样式是否溢出或遮挡。插件从真实DOM生成骨架屏时特殊字体、图片懒加载、组件蒙层都有可能导致生成的骨架屏块过大或错位。生成完要拿手机模拟器过一遍重点看头部导航栏、侧边栏、底部Tab这些常驻结构。第二确认骨架屏不会阻挡用户点击。骨架屏是静态DOM如果某些区域在骨架屏阶段就可以点击比如返回按钮、导航入口骨架屏的灰块会把它们挡住。处理办法是给骨架屏容器设置pointer-events: none让它不拦截任何点击事件用户等得不耐烦想点返回时不会出现点了没反应。第三处理路由切换时的骨架屏刷新。这个插件注入的骨架屏通常只在首次加载时生效路由切换后不会自动出现。如果你希望做全局路由级的骨架屏效果需要配合路由守卫在跳转时重新显示骨架屏。但说实话SPA内部路由切换的等待时间通常很短多做反而容易出现闪烁。自动生成方案的痛点是生成容易、精修难。生成的骨架屏偶尔会出现奇奇怪怪的样式你要去手动改注入的CSS维护成本并不比手写低多少。它的价值在于给了你一个不错的开局剩下的打磨还得靠人。5. 方案四defineAsyncComponent搭配SuspenseVue 3原生方案5.1 异步组件加载期间的fallbackVue 3提供了一套原生的异步组件加载方案defineAsyncComponent配合Suspense组件可以在异步组件加载期间渲染一个fallback插槽。这个fallback就是天然骨架屏的容器。先看一个最基础的写法。页面上有一个数据图表模块这个模块体积较大我希望它在需要时再加载加载期间显示骨架屏template div h1数据大屏/h1 Suspense template #default BigChart / /template template #fallback ChartSkeleton / /template /Suspense /div /template script setup import { defineAsyncComponent } from vue const BigChart defineAsyncComponent(() import(/components/BigChart.vue)) /script其中ChartSkeleton.vue可以是一个手写的、或者基于el-skeleton封装的骨架屏组件。当BigChart还在加载时#fallback插槽里的内容会显示加载完成后Vue自动切换成BigChart。这段代码的核心机制是defineAsyncComponent返回的组件在resolve之前处于pending状态Suspense组件会捕获到这个pending状态并渲染fallback。和方案一的组件级骨架屏不同这里的骨架屏作用于异步组件文件的加载等待时间对首屏的贡献取决于异步组件本身占多大比重。5.2 配合路由懒加载做页面级骨架屏Suspense不只是能包组件还能包路由。如果你用createRouter时配置了异步组件形式的路由配合Suspense也能做到路由级骨架屏// router/index.ts const routes [ { path: /detail/:id, name: Detail, component: () import(/views/Detail.vue), }, ]然后在App.vue里包一层template RouterView v-slot{ Component } Suspense template #default component :isComponent / /template template #fallback PageSkeleton / /template /Suspense /RouterView /template路由懒加载体现在每个路由对应的JS文件会独立分包只有访问到这个路由时才下载对应的JS从而减小首屏JS体积。Suspense负责在下载JS期间显示骨架屏。两者一配合首屏的体积压力和视觉体验都照顾到了。我在实际项目里更推荐这个组合原因有三点一是它是Vue官方原生的能力不引入额外的构建插件升级依赖时的风险小二是骨架屏组件本身是Vue组件可以复用状态、可以按业务定制动态细节不像静态HTML骨架屏那样只能写死三是它天然按路由分裂资源对首屏体积优化的帮助是明摆着的。5.3 配套工程化分包与预加载Vue 3的项目里单纯用defineAsyncComponent还不够配合构建配置才能发挥最大效果。以Vite为例build.rollupOptions.output.manualChunks可以手动把大依赖拆出来// vite.config.ts export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { element-plus: [element-plus], echarts: [echarts], vendor-react: [vue, vue-router, pinia], }, }, }, }, })把echarts、element-plus这种动辄几百KB体积的第三方库拆成独立chunk可以利用浏览器的并发下载提升加载速度也能让核心业务代码更早执行。配合link relpreload预加载关键chunk首屏体验会更好。骨架屏在这一层的意义是分包拆得越细异步组件文件越多Suspense发挥的空间越大。每个路由级组件都配一个骨架屏资源加载与内容展示之间的空窗就会被骨架屏填满。这也是我目前最推荐在Vue 3Vite新项目里采用的方向。6. 四种方案横向对比与选型建议6.1 一表看懂差异为了让大家快速决策我把四种方案的关键维度整理成一张表维度方案一el-skeleton组件方案二index.html手写骨架屏方案三page-skeleton-webpack-plugin方案四SuspensedefineAsyncComponent接入成本极低5分钟中需手工维护HTML/CSS高需配置插件和验证生成效果中需理解Suspense机制首屏白屏覆盖能力弱Vue挂载后才显示强HTML解析即显示强构建时注入HTML中依赖路由/异步组件加载时机样式可控度受UI库限制完全可控生成后需要人工微调完全可控组件化定制路由切换支持局部场景手动实现全局或路由级首次加载为主天然支持页面级切换对Vue版本要求Vue 2/3均可Vue 2/3均可主要支持Vue 2Vue 3原生维护成本低较高页面结构变了要同步改中生成和微调都需要维护中组件需跟随业务迭代SEO友好度低高中中表格看下来方案二在首屏白屏覆盖能力上最强因为它的骨架屏能伴随HTML最早出现方案四在路由切换支持上和工程化分结合上最自然方案一胜在快适合局部场景方案三是自动化的产物适合页面结构复杂、手写成本高但同时能接受插件的维护成本的情况。6.2 不同项目怎么选我建议按项目的技术栈和痛点来选而不是统一套一个模板。如果你维护的是老项目Vue 2 webpack页面结构稳定痛点集中在首屏白屏时间太长直接上方案二。把各业务页的骨架屏用静态HTML维护好成本可控收益直接。别去折腾插件插件在Vue 2老项目里虽然能用但配置成本和版本兼容的突发事件会让你得不偿失。如果你项目用了Element Plus或Ant Design Vue且痛点主要在接口慢、局部数据区块加载让人等得烦躁优先方案一。它的好处是几乎零成本UI风格和你的业务组件天然一致不需要额外设计。如果你是Vue 3 Vite的新项目我强烈建议以方案四为主需要精细化SEO的少数营销页或落地页再叠加方案二。方案四和构建分包的组合能做到的路由级骨架屏体验比手动维护的静态骨架屏更省心。方案三现在更适合你已经有一个Vue 2项目、页面确实复杂、想快速批量生成基础骨架屏再做微调的场景。注意生成完一定要好好检查把它当成起点而不是终点。7. 实战中容易踩的6个坑7.1 骨架屏闪了一下就消失这是最让我头疼的问题。骨架屏明明写了但页面上闪一下极短的时间就没影了跟没写一样。排查思路通常是这几个一是看真实内容是不是渲染太快了如果接口都在本地mock或者CDN缓存里200ms就返回了骨架屏自然一闪而过——这种情况可以降低闪动观感骨架屏的移除加一点过渡动画或者对loading状态设一个最短展示时间比如至少展示500ms。二是看Vue挂载是否覆盖掉了骨架屏方案二里如果挂载点写错骨架屏会被当成普通DOM挂载到页面里不会消失别把闪一下和根本没显示搞混。7.2 插件生成的骨架屏失真方案三生成出来的骨架屏偶尔会让人哭笑不得图片区变成一坨黑色、导航栏高度不对、卡片之间出现莫名其妙的间距。原因多半是页面里存在异步加载的数据图片、动态计算的样式插件截图时拿到的是不完全状态。我的处理经验是生成前先把页面里容易出问题的部分轮播图、懒加载图片、异步图表暂时用一个静态div占位生成完再恢复。这样产出的骨架屏反而更干净。7.3 骨架屏挡住了按钮点击骨架屏是静态DOM如果页面头部有返回按钮、搜索框这些真实交互元素在骨架屏阶段被灰色块盖住用户点返回看起来没反应体验很糟糕。处理办法是给骨架屏容器加pointer-events: none或者把骨架屏的z-index控制在真实交互层之下。但注意如果你真的把点击释放给真实按钮真实按钮可能还没有挂载事件点了依然没反应。所以更好的做法是骨架屏阶段就把可点击区域的块留出来不要用灰色块覆盖直接让背景色露出来。7.4 路由切换时上一页内容残留用方案四做路由级Suspense时有一个我自己踩过的坑路由切换的异步组件还在加载但旧页面已经被销毁页面处于短暂空白等待的状态这个等待看起来比首屏更怪。好在Suspense的fallback在这里能顶上但要注意fallback要覆盖整块路由视图否则内容区以外的部分会露馅。另一个经验是路由级Suspense的fallback应该做成页面级骨架屏而不是小组件级骨架屏结构上要模拟整个路由页面的布局。7.5 骨架屏让LCP数值更难看这是一个比较反直觉的坑。LCP衡量的是最大内容块的渲染时间如果骨架屏中的某个灰块恰好被浏览器识别为最大内容LCP会记录骨架屏灰块的渲染时间而这个时间点往往比真实内容出现的时间早很多。表面看LCP指标变好了但用户真正看到内容的时间并没有变短。这个问题的本质是骨架屏要恰当标记为占位内容不要让它成为LCP元素。给骨架屏设置aria-hiddentrue、rolepresentation以及content-visibility: hidden可以在一定程度上让它不被纳入核心性能指标的核心元素但不同浏览器的行为有差异别过度依赖。更重要的还是配合真实的数据加载链路去优化接口和资源骨架屏是锦上添花不能雪中送炭。7.6 移动端骨架屏动画抖动骨架屏的光扫动画是用background-position或者transform做的在低端安卓机上面容易出现抖动、闪烁。我实测下来两个规避方法比较有效一是用transform: translateX()代替background-position动画因为transform走GPU compositor性能更好二是给骨架屏块加will-change: transform但这个属性用多了会消耗内存只加在关键的头部大块上就好。从第一次在项目里做骨架屏到现在我的一个总体体会是骨架屏优化的是感知但你不能只优化感知。如果接口要3秒才返回无论骨架屏多精细用户还是会觉得慢只是没有之前那么烦躁。真正让我觉得值得做骨架屏的场景是页面结构已经有合理的代码分割、路由懒加载做了、静态资源和接口都优化过一轮之后骨架屏把这些优化之间的缝隙填上让整个加载过程变成渐近呈现而不是空白-突现。如果你也准备做一次Vue首屏优化先把Performance面板的数据记录下来选择一个上面提到的路线跑通后再回到数据面板对比——量化提升远比感觉上有效更重要。
网站建设高端定制企业官网