文章很长不知道读到哪了:加个阅读进度条

举报
静水流深-云海 发表于 2026/10/08 10:51:09 2026/10/08
【摘要】 长文章滚着滚着就忘了自己读到哪、还剩多少。这篇在最顶上加一条进度条:滚动百分比怎么算(分母不是页面总高度,减掉视口高度才是)、为什么用 transform 缩放比改 width 性能好、requestAnimationFrame 节流,以及窗口 resize 后要重算这些细节。

写长文的时候有个挺实际的反馈:读者滚到一半,不知道自己读了百分之多少、还剩多久。加条顶部的进度条就能解决,很多阅读类网站都有。

这个功能小到不能再小,但真要做得流畅,那个百分比的计算方式还真有点讲究。

一、先算百分比

最直觉的想法是"滚过的距离除以页面高度":

const percent = window.scrollY / document.body.scrollHeight

这是错的。 两种算法对比一下:

分母 滚到底时的结果
scrollHeight 永远到不了 100%,通常只有 80% 左右
scrollHeight - innerHeight 正好 100%

原因是页面高度包含了"还没露出来的那一个视口",滚到最底部时 scrollY 永远到不了 scrollHeight。

正确的分母是能滚动的总距离:

const scrollable = document.documentElement.scrollHeight - window.innerHeight
const percent = window.scrollY / scrollable

scrollHeight 是整个文档高度,innerHeight 是视口高度,两者相减才是你真正能滚动的距离。滚到底时 scrollY 正好等于它,百分比刚好 100%。

用 document.documentElement 而不是 document.body,后者在某些布局下高度会算少。

二、渲染出来

顶部一条细线:

<div id="progress" class="progress"></div>
.progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 3px;
  background: #3a72d4;
  transform: scaleX(0);          /* 用缩放,不改 width */
  transform-origin: left;        /* 从左往右长 */
  z-index: 9999;
}
const bar = document.getElementById('progress')

function update() {
  const scrollable = document.documentElement.scrollHeight - window.innerHeight
  const percent = scrollable > 0 ? window.scrollY / scrollable : 0
  bar.style.transform = `scaleX(${percent})`
}

为什么用 transform: scaleX() 而不是改 width?

因为改 width 会触发浏览器的重排(重新计算布局),而 transform 只触发合成,可以交给 GPU 处理,不会引起页面其他元素重新排布。滚动时这个操作每帧都在做,差别就明显了。

这是个通用原则:高频变化的视觉效果,优先用 transform 和 opacity。

三、节流:滚动事件太频繁

滚动时 scroll 事件一秒能触发几十次,每次都读 scrollHeight 还会强制浏览器重新计算布局(这叫"布局抖动",很慢)。

用 requestAnimationFrame 把更新排到下一帧,并且同一帧只做一次:

let ticking = false

window.addEventListener('scroll', () => {
  if (ticking) return
  ticking = true
  requestAnimationFrame(() => {
    update()
    ticking = false
  })
}, { passive: true })

passive: true 告诉浏览器"我不会阻止滚动",让它放心优化。

四、优化:缓存高度

每次都读 scrollHeight 是个消耗(会触发重排)。它其实只在窗口大小变化或者内容增减时才变,可以缓存起来:

let scrollable = 0

function measure() {
  scrollable = document.documentElement.scrollHeight - window.innerHeight
}

function update() {
  const percent = scrollable > 0 ? window.scrollY / scrollable : 0
  bar.style.transform = `scaleX(${percent})`
}

window.addEventListener('resize', measure)
window.addEventListener('load', measure)
measure()

resize 后必须重新测量。 窗口变高了,能滚动的距离就变短了,不重算的话进度条会不准——比如拉高窗口之后,明明滚到底了进度条才显示 60%。

如果文章里有可折叠的内容(展开后高度变了),展开后也要调一次 measure()。

五、几个细节

  • 除以零要防。短文章一屏就显示完了,scrollHeight - innerHeight 等于 0,除以 0 会得到 NaN,进度条直接没了。所以上面写了 scrollable > 0 ? ... : 0。

  • 百分比要限制在 0~1。某些浏览器的"回弹"效果(Mac 上滚过头)会让 scrollY 变成负数或者超出范围:

    const percent = Math.min(1, Math.max(0, window.scrollY / scrollable))
    
  • 移动端地址栏会伸缩。手机上滚动时浏览器地址栏会收起/展开,innerHeight 跟着变,进度条可能轻微跳动。不严重,能接受;讲究的话可以在 resize 时做防抖。

  • 别挡住内容。进度条用 position: fixed 浮在最上层,如果你的页面顶部也有固定导航栏,要注意层级和高度,别叠在一起。

  • 颜色别太扎眼。3px 高的细线,用主题色就行。别做成彩虹渐变还带动画,那是干扰不是帮助。

  • 只在长文章显示。一屏就看完的页面不需要进度条,可以在文章超过一定高度时才初始化。

小结

阅读进度条的代码总共也就二十行,核心是那个计算公式——分母是 scrollHeight - innerHeight,不是 scrollHeight,这一处错了进度条永远走不满。剩下的优化都是"高频更新"场景的通用套路:用 transform 代替 width 避免重排、用 requestAnimationFrame 节流、缓存高度并在 resize 时重算。顺手练一遍这套思路,以后做滚动视差、吸顶导航这类东西都用得上。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。