web滚动性能优化及兼容问题

mouse
如果滚动仅仅是onscroll那么简单那么本文就没有存在的必要了,至于兼容这个老生常谈的问题……还是呵呵吧。

先来说说需求

某日看到某文发现其网站有一很舒适的设计,即当你向下看文章的时候会提示你看到多少了。细心一想这个主意还是蛮有用的,遂「抄袭」之。

某文某网站:http://www.ozy.com/pov/teaching-kids-code/3238

最终效果:http://www.geekpark.net/topics/171430

开撸

呵,不就是这么个小小的功能么,都不用打开我的Sublime,vi都搞定了。

  1. 先画个 div ,然后 position: fixed 到最顶部,然后 width: 0 调整颜色 balabala….

  2. 然后根据js监听浏览器滚动,用已滚动的部分的高度 - 文章距离顶部的高度 = 已阅读部分文章的高度。然后用这个高度除以文章总高度就等于阅读百分比。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$(function() {
var $line = $('#reading_progress'),
$article = $('#article'),
articleH = $article.height(),
articleMt = $article.offset().top;
window.onscroll = render;
function render () {
var readed = $(window).scrollTop() - articleMt,
progress;
// 如果超出阅读则不进行操作
if(readed < 0) {
progress = 0;
} else if(readed > articleH) {
progress = 100;
} else {
progress = parseInt(readed / articleH * 100);
}
$line.css('width', progress + 'px');
}
});

看起来好像咩有什么问题了,于是浏览器刷新调试… 好像和别人的效果不太一样,别人的是舒畅的音乐的感觉,我这一卡一卡的感觉像便秘…

恩,可能是因为没有动画吧。祭出我的杀器:transition: all 0.3s 动画神马必备,看了看效果好像好点了。不过感觉还是有些卡顿,心理作用?

好吧,可能width 触发重绘(repaint)了。

关于浏览器重绘请移步:Avoiding Unnecessary Paints

那不用 width: 100% 如何做到从 0% - 100% 的阅读进度提示呢?另外一个好的方案是 translateX 或者更好的方案 translate3d , 为啥?因为相比 width 的变换,translate 不会引发重绘,这对于性能上有优势。而 translateXtranslate3d 相比,前者已经足够好了但是后者会开启GPU加速,理论上性能更好。所以最后的CSS看起来是酱紫的:

1
2
3
4
5
6
7
8
.reading_progress
width: 100%
height: 3px
position: fixed
z-index: 999
background-color: #7fc042
transform: translate3d(-100%, 0, 0)
transition: all 0.3s

CSS的性能优化到这里就要结束啦,其实 position: fixed 已经引发了重绘,优化 width 什么的都是自我安慰。但在以后的项目中还是尽量少的触发重绘,用户体验毕竟是由众多细小的细节组成的。

接下来聊聊JS,这样写功能上是没有太大问题的。问题在于 onscroll 是否性能足够优秀?

参考该文:
Scrolling Performance
Fixing a parallax scrolling website to run in 60 FPS
Leaner, Meaner, Faster Animations with requestAnimationFrame

夜太长,字太多。不如我来总结一下,总得来说在浏览器滚动的过程中尽量避免图片大小的缩放、bow-shadowwidthheight等变化。在循环动画等问题上尽量避免使用 setTimeout 而是使用 requestAnimationFrame去代替。这和本文有什么关系呢?在我们的 onscroll 中调用的 render()函数会在滚动发生的时候去触发。其实这是很没有必要的,可以通过加锁的形式让其 60fps 的速度去刷新。具体来看代码可能是酱紫的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var didScroll = false;
var ua = navigator.userAgent,
isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
// better way to loop animation
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
if(isMobile) {
$(document).bind('touchmove', updateProgress);
} else {
$(window).on('scroll', updateProgress);
}
function updateProgress() {
didScroll = true;
}
// 这个东西不应该在 updateProgress里么?好吧,我试着放进去然后没动画效果了,原因未知。
// 这样的话即时不滚动也会不停的检查高度信息,但是... 谁看文章不滚动呢...
(function animloop(){
requestAnimFrame(animloop);
if(didScroll) {
didScroll = false;
render();
}
})();

在移动端应该使用 touchmove 事件而不是 onscroll 事件,但即使使用了 touchmove 事件在我的 ios 8.3 chrome version 42.0上仍然有问题,因为Chrome考虑到性能问题跳过了 touchmove事件。通常来说使用 onscroll 事件在众多移动端浏览器上事件触发是在滚动结束后才触发。而PC端是只要滚动就能触发,因此随滚动而产生的动画会比较流畅。但如果是滚动结束后才产生,那就会表现的有点卡。其实不是性能问题。而 touchmove 在移动端却可以连续触发。虽然在某些浏览器上还是有Bug。

onscroll Event Issues on Mobile Browsers
Touch events in Chrome Android

那么这个问题真的没法解决吗? iScroll 的解决办法是用 JS 模拟滚动,而对于本需求来说确实有那么点杀鸡用牛刀的感觉,毕竟不是什么核心需求。所以直到目前位置这个问题依然在 ios android上 没有解决,期待以后的解决方案。

头图来自Dribbble @Jamal