前言
ShokaX和Shoka
Shoka是一款非常美观的Hexo博客主题,本人曾经也使用过该主题。但由于作者长期未更新(最近一次更新在两年前),同时主题本身存在着部分BUG(例如mermaid图标不能显示,CDN被污染等),于是催生出了ShokaX这样的二次开发版。 ShokaX 相比于 Shoka:
- 改变了技术栈:Shoka是JS + Native + Nunjucks,ShokaX是TS + Vue 3 + Pug
- 更改了大量难以访问的CDN链接
- 允许通过注入API以实现自定义功能
- PWA支持
- ...
存在的问题
由于Shoka主题中使用了大量的动画,在文章字数比较少的时候不会有什么问题,但一旦文章字数变多或者存在大量的LaTeX,就会出现卡顿的现象。 例如该文章clrs-book-note大约包含50k个字,内部包含大量的LaTeX,Lighthouse统计大约有45116个DOM元素。在使用Shoka主题时,上下滚动会感受到明显的卡顿,鼠标点击的烟花动画更是只有个位数的帧率,且这种现象在移动端或是较旧的浏览器上会更加明显。
ShokaX vs Shoka
测试文章采用clrs-book-note ShokaX使用v0.2.8,基础配置 Shoka使用v0.2.5,基础配置 浏览器版本:
- Chrome 内核版本 114.0.5735.110
- 360极速浏览器 内核版本 86.0.4240.198
测试方式:尽可能以相同方式滑动网页,通过分析开发者工具中的性能选项卡以进行比较。
Chrome
Shoka主题如下图所示:
平均每个任务耗时约25毫秒
ShokaX主题如下图所示:
平均每个任务耗时约15毫秒
可以看到,如今的chrome内核的优化已经非常好了,两者每次任务也仅仅相差10毫秒,可以说对于帧率没有什么影响。
360极速浏览器
但在内核版本老一些的浏览器,如360极速浏览器中,情况就变得比较糟糕了:
Shoka主题如下图所示:
可以看到几乎所有的任务都是长任务(带红色上标),且帧率非常的不稳定(顶部绿色部分),基本上保持在低位。
ShokaX主题如下图所示:
相比于Shoka,长任务少了很多,且帧率在非长任务阶段提升了不少,(但相比于chrome还是比较逆天,由此可见升级内核非常重要)
这是如何实现的?
减少无意义回流
众所周知,回流的代价远远大于重绘,所以尽可能减少回流非常重要。在ShokaX/Shoka中,移动端相比于桌面端有部分元素不用显示,在上下滚动页面时不需要对其进行修改,以此阻止回流的产生。 如以下代码在移动端不需要被执行:
backToTop.child('span').innerText = scrollPercent
$dom('.percent').changeOrGetWidth(scrollPercent)
所以可以将其改为:
if(backToTop.child('span').innerText !== scrollPercent) {
backToTop.child('span').innerText = scrollPercent
}
if($dom('#sidebar').hasClass('affix') || $dom('#sidebar').hasClass('on')) {
$dom('.percent').changeOrGetWidth(scrollPercent)
}
相关PR: #56
暂停动画
ShokaX/Shoka中动画绝对是一个性能杀手,尤其是ShokaX在导航栏中引入了毛玻璃特效,导致性能进一步降低。但实际上这些动画只需要在可见区域内时播放就能可以了,而并不需要一直播放,造成不必要的性能损失。 几个比较重要(吃性能)的CSS动画:
- 头图放大动画
- 头图和文章交界处波浪动画(非常吃性能
- 代码区向下/上展开箭头动画
- 尾部樱花旋转动画
CSS动画可以通过 animation-play-state 属性来控制其播放和暂停:
.stop-animation {
animation-play-state: paused;
}
判断是否在可见区域可以有多种方法:
getBoundingClientRect()函数获得top属性,但这样需要将其写在scroll监听回调中,且会造成回流。
const { top } = document.getElementById('main').getBoundingClientRect();
if (top >= 0) {
document.querySelectorAll('#imgs .item').forEach(i => {
i.classList.remove('stop-animation');
})
} else {
document.querySelectorAll('#imgs .item').forEach(i => {
i.classList.add('stop-animation');
})
}
IntersectionObserver对象(强烈推荐),性能更好,但兼容性不如上一个方法(ShokaX已经放弃了EOL浏览器的支持,所以没关系)
new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
document.querySelectorAll('.parallax>use').forEach(i => {
i.classList.remove('stop-animation');
})
} else {
document.querySelectorAll('.parallax>use').forEach(i => {
i.classList.add('stop-animation');
})
}
}, {
root: null,
threshold: 0.2
}).observe(document.getElementById('waves'));
但需要注意的是 IntersectionObserver 的 root 和其观察的对象必须是父子关系。
相关PR:
#59
跳过渲染
content-visibility 是一个比较新的CSS属性,将其设置为 auto 后如果该元素与用户不相关,它会跳过元素渲染工作,这可以被运用在长列表和含有大量离线内容的渲染中,用以加快渲染速度,提升首屏性能。
但直接使用该属性可能会造成一些副作用——滚动条会一直抽动,这是因为元素跳过渲染如果不指定其高度的话,高度就是0。
所以需要配合 contain-intrinsic-size 这个属性一起使用:通过指定的元素大小(主要是高度)来确保未渲染子元素仍然占据空间,防止高度塌陷。但实际上有些时候高度是不能准确知道的,这时就需要尽可能估计其高度以获得最佳效果。所以这也是该属性存在的一个弊端:其只适合长表格等每行具有固定高度的元素。
Shoka/ShokaX主题中文本主要是通过 <p></p> 包裹,其高度根据文字数量而发生改变,所以不适合使用该方法,但对于高度基本固定的元素,可以考虑采用如下的CSS:
.waves {
width: 100%;
height: 15vh;
margin-bottom: -.6875rem;
min-height: 3.125rem;
max-height: 9.375rem;
position:relative;
content-visibility: auto;
contain-intrinsic-size: 100vw 15vh;
+mobile() {
height: 10vh;
contain-intrinsic-size: 100vw 10vh;
}
}
相关PR: #44
后记
本文所说的几种优化方法也只是冰山一角,后续ShokaX也许还可以?
- 对于含有大量LaTeX公式的文章也许可以使用渲染成为svg,以减少DOM元素数量?
- 当前图片懒加载使用lozad.js,在加载时会造成强制重排和布局切换,是否可以防止这一切?
- ...