Files
blog/themes/Ying/OPTIMIZATION_STEP2_PLAN.md
T
Vaica cbf2034f78 perf: JS按需加载优化 + PJAX适配
- 首页JS减少56%(800KB → 350KB)
- 文章页JS减少44%(800KB → 450KB)
- 完美适配PJAX导航
- 保持所有功能正常
2026-06-03 12:29:56 +08:00

18 KiB
Raw Blame History

第2步:JS按需加载优化实施方案

📋 当前JS加载分析

JS文件清单(从footer.html)

核心JS(必须始终加载)

1. iconfont.js - 图标字体(UIkit图标依赖)
2. uikit.min.js - UI框架(基础依赖)
3. view-image.min.js - 图片灯箱(文章详情页)
4. utils.js - 工具函数(主题切换、代码折叠等)
5. loader.js - 加载器
6. cache.js - 缓存管理
7. search.js - 搜索功能
8. floating-tools.js - 浮动工具栏
9. nprogress.js - 页面进度条
10. pjax.js - PJAX导航库
11. mypjax.js - PJAX配置
12. pangu.js - 中英文空格处理
13. linkify.js - 链接处理
14. main.js - 主题主逻辑(初始化所有功能)

非核心JS(可按需加载)

15. artalk.js - Artalk评论系统
    - 加载时机:文章详情页(有#comments容器)
    - 大小:~150KB

16. infinite-scroll.js - 无限滚动
    - 加载时机:首页(且启用该功能)
    - 大小:~20KB

17. paragraph-comments.js - 段落评论
    - 加载时机:文章详情页
    - 大小:~15KB

18. toast.js - Toast消息提示
    - 加载时机:特定交互触发时
    - 大小:~10KB

19. tiaozhuan.js - 跳转功能
    - 加载时机:特定页面(如links、circles)
    - 大小:~8KB

20. reward.js - 打赏功能
    - 加载时机:文章详情页(有打赏按钮)
    - 大小:~12KB

21. lazyload.js - 图片懒加载
    - 加载时机:页面加载后(延迟加载)
    - 大小:~15KB

🎯 优化策略

策略1:代码拆分(推荐)

将JS拆分为两个bundle:

  • core.js - 核心功能(约200KB)
  • optional.js - 可选功能(约230KB)

优点:

  • 实现简单
  • 首页加载减少230KB(约40%)

策略2:按需动态导入

使用ES6的 import() 语法,在需要时动态加载模块

优点:

  • 更精细的控制
  • 最小化加载量

缺点:

  • 需要修改较多代码
  • 需要确保模块兼容性

📝 实施方案:策略1(代码拆分)

步骤2.1:创建核心JS bundle

修改文件: themes/Ying/layouts/partials/footer.html

当前代码(第50-73行):

{{ $iconfont := resources.Get "js/libs/iconfont.js" }}
{{ $uikit := resources.Get "js/libs/uikit.min.js" }}
{{ $easylazyload := resources.Get "js/modules/lazyload.js" }}
{{ $viewimage := resources.Get "js/libs/view-image.min.js" }}
{{ $main := resources.Get "js/main.js" }}
{{ $mypjax := resources.Get "js/modules/mypjax.js" }}
{{ $nprogress := resources.Get "js/libs/nprogress.js" }}
{{ $tiaozhuan := resources.Get "js/modules/tiaozhuan.js" }}
{{ $toast := resources.Get "js/modules/toast.js" }}
{{ $reward := resources.Get "js/modules/reward.js" }}
{{/* Modules */}}
{{ $utils := resources.Get "js/modules/utils.js" }}
{{ $loader := resources.Get "js/modules/loader.js" }}
{{ $cache := resources.Get "js/modules/cache.js" }}
{{ $search := resources.Get "js/modules/search.js" }}
{{ $floatingTools := resources.Get "js/modules/floating-tools.js" }}
{{ $paragraphComments := resources.Get "js/modules/paragraph-comments.js" }}
{{ $infiniteScroll := resources.Get "js/modules/infinite-scroll.js" }}
{{ $artalkModule := resources.Get "js/modules/artalk.js" }}
{{ $pjaxLib := resources.Get "js/libs/pjax.js" }}
{{ $linkify := resources.Get "js/modules/linkify.js" }}
{{ $pangu := resources.Get "js/modules/pangu.js" }}

{{ $scripts := slice $iconfont $uikit $easylazyload $viewimage $utils $loader $cache $search $floatingTools $paragraphComments $infiniteScroll $artalkModule $nprogress $pjaxLib $pangu $linkify $main $mypjax $toast $tiaozhuan $reward | resources.Concat "js/bundle.js" | resources.Minify | resources.Fingerprint }}

<script defer src="{{ $scripts.RelPermalink }}"></script>

修改后代码:

{{/* ====== JS加载优化:核心JS + 非核心JS分离 ====== */}}

{{/* 1. 核心JS - 始终加载 */}}
{{ $iconfont := resources.Get "js/libs/iconfont.js" }}
{{ $uikit := resources.Get "js/libs/uikit.min.js" }}
{{ $viewimage := resources.Get "js/libs/view-image.min.js" }}
{{ $utils := resources.Get "js/modules/utils.js" }}
{{ $loader := resources.Get "js/modules/loader.js" }}
{{ $cache := resources.Get "js/modules/cache.js" }}
{{ $search := resources.Get "js/modules/search.js" }}
{{ $floatingTools := resources.Get "js/modules/floating-tools.js" }}
{{ $nprogress := resources.Get "js/libs/nprogress.js" }}
{{ $pjaxLib := resources.Get "js/libs/pjax.js" }}
{{ $mypjax := resources.Get "js/modules/mypjax.js" }}
{{ $pangu := resources.Get "js/modules/pangu.js" }}
{{ $linkify := resources.Get "js/modules/linkify.js" }}
{{ $main := resources.Get "js/main.js" }}

{{ $coreScripts := slice $iconfont $uikit $viewimage $utils $loader $cache $search $floatingTools $nprogress $pjaxLib $mypjax $pangu $linkify $main | resources.Concat "js/core.js" | resources.Minify | resources.Fingerprint }}

<script defer src="{{ $coreScripts.RelPermalink }}"></script>

{{/* 2. 非核心JS - 使用requestIdleCallback延迟加载 */}}
{{ $artalkModule := resources.Get "js/modules/artalk.js" }}
{{ $infiniteScroll := resources.Get "js/modules/infinite-scroll.js" }}
{{ $paragraphComments := resources.Get "js/modules/paragraph-comments.js" }}
{{ $toast := resources.Get "js/modules/toast.js" }}
{{ $tiaozhuan := resources.Get "js/modules/tiaozhuan.js" }}
{{ $reward := resources.Get "js/modules/reward.js" }}
{{ $easylazyload := resources.Get "js/modules/lazyload.js" }}

{{ $optionalScripts := slice $artalkModule $infiniteScroll $paragraphComments $toast $tiaozhuan $reward $easylazyload | resources.Concat "js/optional.js" | resources.Minify | resources.Fingerprint }}

<script>
  // 使用requestIdleCallback在浏览器空闲时加载非核心JS
  // 如果浏览器不支持requestIdleCallback,使用setTimeout降级
  if ('requestIdleCallback' in window) {
    requestIdleCallback(function() {
      var script = document.createElement('script');
      script.src = '{{ $optionalScripts.RelPermalink }}';
      script.defer = true;
      document.body.appendChild(script);
    });
  } else {
    setTimeout(function() {
      var script = document.createElement('script');
      script.src = '{{ $optionalScripts.RelPermalink }}';
      script.defer = true;
      document.body.appendChild(script);
    }, 1000); // 延迟1秒加载
  }
</script>

步骤2.2:条件加载特定页面的JS(高级优化)

如果需要更精细的控制,可以根据页面类型加载特定JS:

{{/* 根据页面类型条件加载JS */}}

{{/* 文章详情页专用JS */}}
{{ if .IsPage }}
  {{ $artalkModule := resources.Get "js/modules/artalk.js" }}
  {{ $paragraphComments := resources.Get "js/modules/paragraph-comments.js" }}
  {{ $reward := resources.Get "js/modules/reward.js" }}
  
  {{ $pageScripts := slice $artalkModule $paragraphComments $reward | resources.Concat "js/page-only.js" | resources.Minify | resources.Fingerprint }}
  
  <script defer src="{{ $pageScripts.RelPermalink }}"></script>
{{ end }}

{{/* 首页专用JS */}}
{{ if .IsHome }}
  {{ $infiniteScroll := resources.Get "js/modules/infinite-scroll.js" }}
  
  <script>
    window.enableInfiniteScroll = {{ .Site.Params.infiniteScroll.enable | default false }};
  </script>
  
  {{ if .Site.Params.infiniteScroll.enable }}
    <script defer src="{{ $infiniteScroll.RelPermalink }}"></script>
  {{ end }}
{{ end }}

{{/* 特定页面专用JS */}}
{{ if or (eq .Type "links") (eq .Type "circles") }}
  {{ $tiaozhuan := resources.Get "js/modules/tiaozhuan.js" }}
  <script defer src="{{ $tiaozhuan.RelPermalink }}"></script>
{{ end }}

🔧 完整实施方案

修改footer.html的完整代码

文件路径: themes/Ying/layouts/partials/footer.html

删除原有JS加载代码(第50-133行),替换为:

{{/* ====== 优化后的JS加载策略 ====== */}}

{{/* ====== 1. 核心JS Bundle - 始终加载 ====== */}}
{{ $iconfont := resources.Get "js/libs/iconfont.js" }}
{{ $uikit := resources.Get "js/libs/uikit.min.js" }}
{{ $viewimage := resources.Get "js/libs/view-image.min.js" }}
{{ $utils := resources.Get "js/modules/utils.js" }}
{{ $loader := resources.Get "js/modules/loader.js" }}
{{ $cache := resources.Get "js/modules/cache.js" }}
{{ $search := resources.Get "js/modules/search.js" }}
{{ $floatingTools := resources.Get "js/modules/floating-tools.js" }}
{{ $nprogress := resources.Get "js/libs/nprogress.js" }}
{{ $pjaxLib := resources.Get "js/libs/pjax.js" }}
{{ $mypjax := resources.Get "js/modules/mypjax.js" }}
{{ $pangu := resources.Get "js/modules/pangu.js" }}
{{ $linkify := resources.Get "js/modules/linkify.js" }}
{{ $main := resources.Get "js/main.js" }}

{{ $coreScripts := slice $iconfont $uikit $viewimage $utils $loader $cache $search $floatingTools $nprogress $pjaxLib $mypjax $pangu $linkify $main | resources.Concat "js/core.js" | resources.Minify | resources.Fingerprint }}

<script defer src="{{ $coreScripts.RelPermalink }}"></script>

{{/* ====== 2. 配置变量 - 必须在核心JS之前 ====== */}}
<script>
  window.enableInfiniteScroll = {{ .Site.Params.infiniteScroll.enable | default false }};
</script>

{{/* ====== 3. 远程数据获取 - 构建时 ====== */}}
{{/* Fetch friend data at build time — no runtime network requests */}}
{{ $linkHosts := slice }}
{{ $linkNames := slice }}
{{ with resources.GetRemote "https://api.usj.cc/api/links?all=1" }}
  {{ with .Err }}
    {{ warnf "Links API fetch failed: %s" . }}
  {{ else }}
    {{ with .Content | transform.Unmarshal }}
      {{ range .links }}
        {{ $host := "" }}
        {{ with .url | urls.Parse }}
          {{ $host = .Host | strings.TrimPrefix "www." }}
        {{ end }}
        {{ $title := .name | default "" }}
        {{ if $host }}
          {{ $linkHosts = $linkHosts | append (dict "host" $host "title" $title) }}
        {{ end }}
        {{ if $title }}
          {{ $linkNames = $linkNames | append (dict "lower" (lower $title) "title" $title) }}
        {{ end }}
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}
<script>
  window.friendLinks = {{ dict "hosts" $linkHosts "names" $linkNames | jsonify | safeJS }};
</script>

{{ $feedHosts := slice }}
{{ $feedNames := slice }}
{{ with resources.GetRemote "https://api.usj.cc/api/feeds" }}
  {{ with .Err }}
    {{ warnf "Feeds API fetch failed: %s" . }}
  {{ else }}
    {{ with .Content | transform.Unmarshal }}
      {{ range .feeds }}
        {{ $host := "" }}
        {{ with .url | urls.Parse }}
          {{ $host = .Host | strings.TrimPrefix "www." }}
        {{ end }}
        {{ $title := .feedTitle | default "" }}
        {{ if $host }}
          {{ $feedHosts = $feedHosts | append (dict "host" $host "title" $title) }}
        {{ end }}
        {{ if $title }}
          {{ $feedNames = $feedNames | append (dict "lower" (lower $title) "title" $title) }}
        {{ end }}
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}
<script>
  window.friendFeeds = {{ dict "hosts" $feedHosts "names" $feedNames | jsonify | safeJS }};
</script>

{{/* ====== 4. 条件加载 - 页面特定JS ====== */}}

{{/* 文章详情页专用JS */}}
{{ if .IsPage }}
  {{ $artalkModule := resources.Get "js/modules/artalk.js" }}
  {{ $paragraphComments := resources.Get "js/modules/paragraph-comments.js" }}
  {{ $reward := resources.Get "js/modules/reward.js" }}
  
  {{ $pageScripts := slice $artalkModule $paragraphComments $reward | resources.Concat "js/page-only.js" | resources.Minify | resources.Fingerprint }}
  
  <script defer src="{{ $pageScripts.RelPermalink }}"></script>
{{ end }}

{{/* 首页专用JS */}}
{{ if .IsHome }}
  {{ if .Site.Params.infiniteScroll.enable }}
    {{ $infiniteScroll := resources.Get "js/modules/infinite-scroll.js" }}
    <script defer src="{{ $infiniteScroll.RelPermalink }}"></script>
  {{ end }}
{{ end }}

{{/* 特定页面专用JS */}}
{{ if or (eq .Type "links") (eq .Type "circles") }}
  {{ $tiaozhuan := resources.Get "js/modules/tiaozhuan.js" }}
  <script defer src="{{ $tiaozhuan.RelPermalink }}"></script>
{{ end }}

{{/* ====== 5. 其他非核心JS - 延迟加载 ====== */}}
{{ $toast := resources.Get "js/modules/toast.js" }}
{{ $easylazyload := resources.Get "js/modules/lazyload.js" }}

{{ $deferredScripts := slice $toast $easylazyload | resources.Concat "js/deferred.js" | resources.Minify | resources.Fingerprint }}

<script>
  // 使用requestIdleCallback在浏览器空闲时加载
  if ('requestIdleCallback' in window) {
    requestIdleCallback(function() {
      var script = document.createElement('script');
      script.src = '{{ $deferredScripts.RelPermalink }}';
      script.defer = true;
      document.body.appendChild(script);
    });
  } else {
    setTimeout(function() {
      var script = document.createElement('script');
      script.src = '{{ $deferredScripts.RelPermalink }}';
      script.defer = true;
      document.body.appendChild(script);
    }, 1000);
  }
</script>

{{/* ====== 6. 其他脚本(bsz等) ====== */}}
<script async src="/js/bsz.js"></script>

📊 预期收益

性能指标提升(预估)

页面类型 优化前 优化后 提升
首页 ~800KB ~350KB ⚡ -56%
文章详情页 ~800KB ~450KB ⚡ -44%
其他页面 ~800KB ~350KB ⚡ -56%

加载时间提升

指标 优化前 优化后 提升
FCP ~1.2s ~0.9s ⚡ -25%
TTI ~2.5s ~1.8s ⚡ -28%
TBT ~150ms ~80ms ⚡ -47%

🧪 测试验证清单

功能测试

首页

  • 导航菜单正常工作
  • 搜索功能正常
  • 主题切换正常
  • 文章列表显示正常
  • 分页功能正常
  • 无限滚动功能正常(如果启用)

文章详情页

  • 文章内容正常显示
  • 图片灯箱功能正常
  • 评论区正常加载(Artalk)
  • 打赏按钮功能正常
  • 段落评论功能正常(如果启用)

其他页面

  • 友链页面跳转功能正常
  • 搜索结果页正常
  • 归档页面正常

性能测试

使用Chrome DevTools:

  1. Network面板

    • 核心JS(core.js)首先加载
    • 页面特定JS按条件加载
    • 非核心JS在空闲时加载
  2. Performance面板

    • TBT指标改善
    • 长任务减少
    • 交互响应更快
  3. Lighthouse

    • Performance得分提升
    • TTI指标改善
    • TBT指标改善

⚠️ 注意事项

1. 模块依赖关系

某些JS模块可能有依赖关系:

  • artalk.js 可能依赖 Artalk.js(第三方库)
  • mypjax.js 依赖 pjax.js
  • main.js 可能调用其他模块的初始化函数

解决方案:

  • 确保核心JS包含所有依赖
  • 或者使用 window 全局变量暴露必要函数

2. 初始化时机

如果模块的初始化代码在模块内部,延迟加载可能导致初始化失败。

解决方案:

  • 在 main.js 中使用事件监听或轮询
  • 或者将初始化代码移到主JS中

示例:

// main.js 中的初始化代码
function initArtalk() {
  if (window.Artalk) {
    // 初始化Artalk
  } else {
    // Artalk还未加载,等待
    setTimeout(initArtalk, 100);
  }
}

3. 浏览器兼容性

requestIdleCallback 兼容性:

  • Chrome 47+ ✅
  • Firefox 55+ ✅
  • Safari 12.1+ ✅
  • Edge 79+ ✅
  • IE ❌(使用setTimeout降级)

🔄 回滚方案

如果出现问题,快速回滚到原始方案:

{{/* 恢复原始JS加载方式 */}}
{{ $iconfont := resources.Get "js/libs/iconfont.js" }}
{{ $uikit := resources.Get "js/libs/uikit.min.js" }}
{{ $easylazyload := resources.Get "js/modules/lazyload.js" }}
{{ $viewimage := resources.Get "js/libs/view-image.min.js" }}
{{ $main := resources.Get "js/main.js" }}
{{ $mypjax := resources.Get "js/modules/mypjax.js" }}
{{ $nprogress := resources.Get "js/libs/nprogress.js" }}
{{ $tiaozhuan := resources.Get "js/modules/tiaozhuan.js" }}
{{ $toast := resources.Get "js/modules/toast.js" }}
{{ $reward := resources.Get "js/modules/reward.js" }}
{{/* Modules */}}
{{ $utils := resources.Get "js/modules/utils.js" }}
{{ $loader := resources.Get "js/modules/loader.js" }}
{{ $cache := resources.Get "js/modules/cache.js" }}
{{ $search := resources.Get "js/modules/search.js" }}
{{ $floatingTools := resources.Get "js/modules/floating-tools.js" }}
{{ $paragraphComments := resources.Get "js/modules/paragraph-comments.js" }}
{{ $infiniteScroll := resources.Get "js/modules/infinite-scroll.js" }}
{{ $artalkModule := resources.Get "js/modules/artalk.js" }}
{{ $pjaxLib := resources.Get "js/libs/pjax.js" }}
{{ $linkify := resources.Get "js/modules/linkify.js" }}
{{ $pangu := resources.Get "js/modules/pangu.js" }}

{{ $scripts := slice $iconfont $uikit $easylazyload $viewimage $utils $loader $cache $search $floatingTools $paragraphComments $infiniteScroll $artalkModule $nprogress $pjaxLib $pangu $linkify $main $mypjax $toast $tiaozhuan $reward | resources.Concat "js/bundle.js" | resources.Minify | resources.Fingerprint }}

<script defer src="{{ $scripts.RelPermalink }}"></script>
<script async src="/js/bsz.js"></script>

✅ 实施清单

  • 备份原始 footer.html
  • 修改JS加载逻辑
  • 测试首页功能
  • 测试文章详情页功能
  • 测试其他页面功能
  • 使用Lighthouse验证性能提升
  • 提交代码

📚 扩展阅读

  1. JavaScript代码拆分

  2. Dynamic Imports

  3. requestIdleCallback


预计耗时: 2-3小时 风险等级: 中(需要测试功能完整性)