优先级提示在前端性能优化中的实战应用与原理剖析

恒豪酱~ 优化 阅读 2,682
赞 18 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个月搞一个内容密集型的中后台系统,页面里塞了十几张图表、一堆懒加载图片、还有几个实时数据卡片。用户反馈“打开慢得像卡碟”,我一开始以为是接口慢,结果 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 是个轻量又有效的优化手段,特别适合内容结构清晰的页面。它不是银弹,但花半小时改造,换几百分之一秒的性能提升,性价比很高。

以上是我在这个项目里折腾优先级提示的完整过程,中间踩的坑都列出来了。如果你有更好的动态优先级方案,或者在其他资源类型上玩出花来了,欢迎评论区交流!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论