文章很长不知道读到哪了:加个阅读进度条
写长文的时候有个挺实际的反馈:读者滚到一半,不知道自己读了百分之多少、还剩多久。加条顶部的进度条就能解决,很多阅读类网站都有。
这个功能小到不能再小,但真要做得流畅,那个百分比的计算方式还真有点讲究。
一、先算百分比
最直觉的想法是"滚过的距离除以页面高度":
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 时重算。顺手练一遍这套思路,以后做滚动视差、吸顶导航这类东西都用得上。
- 点赞
- 收藏
- 关注作者
评论(0)