# 前言
# 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,在加载时会造成强制重排和布局切换,是否可以防止这一切?
- ...