优先级提示在前端性能优化中的实战应用与原理剖析
项目初期的技术选型
上个月搞一个内容密集型的中后台系统,页面里塞了十几张图表、一堆懒加载图片、还有几个实时数据卡片。用户反馈“打开慢得像卡碟”,我一开始以为是接口慢,结果 Network 面板一开,发现主包加载完之后,浏览器还在吭哧吭哧解析一堆低优先级资源,比如页脚的小图标、非首屏的装饰图——这些玩意儿居然和核心业务组件抢带宽。
这时候就想到了 fetchpriority(以前叫 importance),这玩意儿在 Chrome 103+ 已经能用了。虽然兼容性不算完美,但我们的用户基本都是企业内网环境,Chrome 版本可控,值得一试。目标很明确:让关键资源先加载,非关键资源往后排。
核心代码就这几行
最开始以为加个属性就行,比如:
<img src="hero-banner.jpg" fetchpriority="high" alt="主图">
<img src="footer-icon.png" fetchpriority="low" alt="页脚小图标">
但实际项目哪有这么简单。我们用的是 Vue + Vite,图片大多是动态路径,而且很多是通过组件 props 传进来的。所以得封装一个带优先级提示的图片组件。
<template>
<img
:src="src"
:alt="alt"
:fetchpriority="priority"
@load="onLoad"
@error="onError"
/>
</template>
<script setup>
const props = defineProps({
src: { type: String, required: true },
alt: { type: String, default: '' },
priority: {
type: String,
validator: (val) => ['high', 'low', 'auto'].includes(val),
default: 'auto'
}
})
const emit = defineEmits(['load', 'error'])
const onLoad = (e) => emit('load', e)
const onError = (e) => emit('error', e)
</script>
然后在业务组件里这样用:
<HeroImage
:src="https://jztheme.com/assets/hero-${currentTheme}.jpg"
priority="high"
/>
<FooterIcon
src="https://jztheme.com/assets/icon-footer.svg"
priority="low"
/>
看起来挺顺,但真正麻烦的在后面。
最大的坑:动态优先级失效
项目里有个需求:用户切换主题时,主图要换。我一开始直接绑定 priority 到响应式变量:
const currentPriority = computed(() => isHeroVisible ? 'high' : 'low')
结果发现,一旦图片元素已经渲染,再改 fetchpriority 属性完全没用。浏览器在元素首次插入 DOM 时就决定了资源优先级,后续修改属性不会触发重新调度。这坑我踩了整整一下午,F12 看 Initiator 和 Priority 列,死活不变。
折腾了半天,最后只能用“暴力刷新”:当优先级需要变更时,强制销毁并重建 img 元素。Vue 里靠 :key 实现:
<template>
<img
:key="${src}-${priority}"
:src="src"
:fetchpriority="priority"
...
/>
</template>
这样每次 priority 变化,Vue 就会干掉旧节点,创建新节点,浏览器也就重新评估优先级了。虽然有点糙,但亲测有效。当然,代价是图片会闪一下(重新加载),所以我们只在确实需要动态调整优先级的场景才这么干,比如从“低优先级预加载”切换到“高优先级立即展示”。
另一个隐藏雷区:和懒加载冲突
我们之前用了 loading="lazy" 做图片懒加载。问题来了:fetchpriority="high" 和 loading="lazy" 同时存在时,浏览器怎么处理?
实测发现,Chrome 会忽略 loading="lazy"
,直接高优加载。这其实是好事,说明优先级提示的权重更高。但 Safari 的行为不太一致(虽然我们不用管 Safari),所以保险起见,我们在逻辑上做了互斥:
// 如果设了 high,就不加 lazy
const shouldLazy = priority !== 'high' && isInViewport === false
这样避免潜在的兼容性问题。
最终的解决方案
综合下来,我们的策略是:
- 首屏核心内容(主图、关键数据卡片):显式设置
fetchpriority="high" - 首屏但非核心(装饰性 SVG、次要图标):保持默认(
auto) - 非首屏内容:设置
fetchpriority="low",同时配合loading="lazy" - 动态内容:用
:key强制重建元素来更新优先级
另外,对 JS 动态 import 也做了处理。比如某个重型图表组件只在特定 tab 才加载:
// 默认低优先级预加载
const ChartHeavy = () => import(/* webpackChunkName: "chart-heavy" */ './ChartHeavy.vue')
// 当用户即将进入该 tab 时,提前高优加载
if (userAboutToOpenChartTab) {
const highPrioImport = document.createElement('link')
highPrioImport.rel = 'prefetch'
highPrioImport.as = 'script'
highPrioImport.href = '/assets/chart-heavy.js'
highPrioImport.fetchpriority = 'high' // 注意:这里对 link 标签也生效
document.head.appendChild(highPrioImport)
}
不过要注意,<link rel="prefetch"> 的 fetchpriority 支持度比 <img> 更差一点,得看具体浏览器。
回顾与反思
效果还是明显的。Lighthouse 的 LCP(最大内容绘制)从 3.2s 降到了 2.1s,FCP(首次内容绘制)也有小幅提升。用户反馈“打开快了不少”,尤其是弱网环境下感知更强。
但有几个地方现在想想还是不够优雅:
- 动态优先级靠重建元素实现,体验上有轻微闪烁,如果浏览器原生支持动态优先级就好了
- 对字体文件、CSS 资源没法直接用
fetchpriority,只能靠<link>预加载,但预加载本身又有额外请求开销 - 有些第三方 SDK 插入的资源(比如埋点脚本)没法控制优先级,它们偶尔还是会拖慢主线程
总的来说,fetchpriority 是个轻量又有效的优化手段,特别适合内容结构清晰的页面。它不是银弹,但花半小时改造,换几百分之一秒的性能提升,性价比很高。
以上是我在这个项目里折腾优先级提示的完整过程,中间踩的坑都列出来了。如果你有更好的动态优先级方案,或者在其他资源类型上玩出花来了,欢迎评论区交流!

暂无评论