Hexo博客性能优化 ShokaX vs Shoka

180 阅读5分钟

前言

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主题如下图所示: chrome-shoka 平均每个任务耗时约25毫秒 ShokaX主题如下图所示: chrome-shokax 平均每个任务耗时约15毫秒

可以看到,如今的chrome内核的优化已经非常好了,两者每次任务也仅仅相差10毫秒,可以说对于帧率没有什么影响。

360极速浏览器

但在内核版本老一些的浏览器,如360极速浏览器中,情况就变得比较糟糕了: Shoka主题如下图所示: 360-shoka 可以看到几乎所有的任务都是长任务(带红色上标),且帧率非常的不稳定(顶部绿色部分),基本上保持在低位。 ShokaX主题如下图所示: 360-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'));

但需要注意的是 IntersectionObserverroot 和其观察的对象必须是父子关系。 相关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,在加载时会造成强制重排和布局切换,是否可以防止这一切?
  • ...