Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【腾讯云前端性能优化大赛】秒开的艺术:Hexo 博客首屏耗时优化实践

【腾讯云前端性能优化大赛】秒开的艺术:Hexo 博客首屏耗时优化实践

原创
作者头像
电脑星人
发布于 2021-12-30 19:42:24
发布于 2021-12-30 19:42:24
9770
举报
文章被收录于专栏:电脑星人电脑星人

前言

Hexo 是一款基于 Node.js 的静态博客生成器。有别于传统的 WordPress、Typecho 等由服务端渲染的动态博客程序,Hexo 可以遍历博客的各个页面,将博客文章等内容渲染到主题(即页面模板)之中,生成全部页面的 HTML 文件及其引用的 CSS、JS 等静态资源。这些静态资源文件常常通过托管到 Pages、托管到对象存储或者自建 Nginx 服务器的方式来对外提供访问。

基于 Hexo 搭建的博客固然免去了服务端重复渲染同一个页面的时间与计算资源开销,但是也将更多的模块和页面逻辑移动到了前端页面之中。不同的博主对于博客的功能需求是各不相同,因此主题的各个可选功能也常常是模块化的,需要引入诸多 JS、CSS、图片和字体等静态资源。Hexo 博客页面及其依赖的静态资源的加载、缓存策略,很大程度上影响着 Hexo 博客的访问体验,以下对其中一些优化方法进行阐述。

避免资源加载引起的阻塞

HTML 页面常常通过 <link rel="stylesheet" href> 以及 <script src> 标签引入 CSS 及 JS 文件,在被引用的资源加载期间,浏览器对后续 HTML 内容的解析和渲染会被阻塞,如果资源在页面的头部引入且加载过于缓慢,则会显著增加白屏时长。

代码语言:txt
AI代码解释
复制
<link rel="stylesheet" href="/css/style/main.css">

<!-- 加载缓慢的 CSS -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap" rel="stylesheet">

我的站点最初直接引入了 Google Fonts 提供的中文字体,这需要加载一个比较大的 CSS,显著延迟了页面完成加载的时间。这部分字体样式不是页面展示所必须的,因此可以尝试让浏览器延迟加载该 CSS 样式文件,具体的做法如下:

  1. 向 link 标签增加 media 属性,值为 only x(这个值在浏览器的媒体查询中与当前页面不匹配,浏览器仍会加载这个 CSS 文件,但不会去使用它,因此也不会阻塞页面的渲染)
  2. 向 link 标签增加 onload 属性,这会在浏览器完成 CSS 的加载后被执行。其中进行两个步骤:(1) 清除掉 onload 回调,避免重复执行; (2) 将 media 属性的值置为 all,这会使得浏览器将这个 CSS 应用到页面中。
代码语言:txt
AI代码解释
复制
<!-- CSS 加载时不会阻塞页面渲染 -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap" rel="stylesheet" media="only x" onload="this.onload=null;this.media='all'">

同理,避免 JS 文件的加载对页面渲染的阻塞,也可以优化页面的加载速度。当 script 标签带有 defer 属性或 async 属性时,JS 文件的加载不会造成页面渲染的阻塞。

  • defer 属性:浏览器会请求该 JS 文件,但会推迟到文档完成解析后,触发 DOMContentLoaded 事件之前才执行
  • async 属性:浏览器并行请求带有 async 属性的 JS 文件,并尽快解析和执行

向 script 标签添加 defer 或 async 属性,要根据 JS 脚本的功能与必要性来确定。比如,用来统计页面访问量的 JS 脚本可以添加 async 属性(不依赖 DOM 结构,也不必立即执行);用于渲染评论区的 JS 脚本可以添加 defer 属性(可以提前加载,且可以等待直到 DOM 加载完成时才执行)。

静态资源版本控制

缓存是提高页面加载速度的一个重点。重复加载已经加载过的静态资源文件,无疑会浪费宝贵的时间与带宽。传统的基于 HTTP 缓存头的缓存策略是通过强制缓存一段时间,以及通过修改时间、ETag 来判断服务器上的文件是否已经被修改。在以下两种情况中,这一套缓存策略的表现不佳:

  • 在强制缓存的 max-age 时间内,服务器上的文件发生了变更,但浏览器仍然使用旧的文件(导致静态资源更新不及时,或多个静态资源之间有不一致)
  • 本地缓存过期,浏览器重新请求服务器,但服务器上的文件实际上没有发生变化。(需要耗费一次往返的时间才能确定本地缓存的静态资源可以使用)

一种静态资源的版本控制方法是向文件名中添加文件内容的哈希值。比如原文件路径为 css/style.css,其哈希值的前8位为 1234abcd,那么添加了哈希值的文件路径变为 css/style.1234abcd.css。这样做的好处是,当文件内容发生变化时,文件名必定发生变化,反过来说,当浏览器已经缓存了该路径的文件,则可以推断缓存的文件在服务器侧没有发生变化,浏览器可以直接使用缓存的版本而不用进行缓存协商(通过设置比较长的强制缓存 max-age 来实现)。

在 Hexo 博客中要实现这种文件版本控制方法,一方面要在 Hexo 构建时修改静态资源的文件名以及对应的引用路径,另一方面要为带哈希值的静态资源设置一个较长的缓存时间,从而实现有效的缓存。

Hexo 支持通过自定义 JS 脚本(放置在 scripts/ 目录中)对 Hexo 的功能进行扩展,我们可以通过 hexo.extend.filter.register("after_generate", callback) 钩子,在 Hexo 生成全部静态文件后对这些文件进行增删改等处理,来实现上述替换静态文件名的操作。具体的代码实现如下:

代码语言:txt
AI代码解释
复制
const hasha = require("hasha");
const minimatch = require("minimatch");

function stream2buffer(stream) {
  return new Promise((resolve, reject) => {
    const _buf = [];
    stream.on("data", (chunk) => _buf.push(chunk));
    stream.on("end", () => resolve(Buffer.concat(_buf)));
    stream.on("error", (err) => reject(err));
  });
}

const readFileAsBuffer = (filePath) => {
  return stream2buffer(hexo.route.get(filePath));
};

const readFileAsString = async (filePath) => {
  const buffer = await readFileAsBuffer(filePath);
  return buffer.toString();
};

const parseFilePath = (filePath) => {
  const parts = filePath.split("/");
  const originalFileName = parts[parts.length - 1];

  const dotPosition = originalFileName.lastIndexOf(".");

  const dirname = parts.slice(0, parts.length - 1).join("/");
  const basename =
    dotPosition === -1
      ? originalFileName
      : originalFileName.substring(0, dotPosition);
  const extension =
    dotPosition === -1 ? "" : originalFileName.substring(dotPosition);

  return [dirname, basename, extension];
};

const genFilePath = (dirname, basename, extension) => {
  let dirPrefix = "";
  if (dirname) {
    dirPrefix += dirname + "/";
  }

  if (extension && !extension.startsWith(".")) {
    extension = "." + extension;
  }

  return dirPrefix + basename + extension;
};

const getRevisionedFilePath = (filePath, revision) => {
  const [dirname, basename, extension] = parseFilePath(filePath);
  return genFilePath(dirname, `${basename}.${revision}`, extension);
};

const revisioned = (filePath) => {
  return getRevisionedFilePath(filePath, `!!revision:${filePath}!!`);
};

hexo.extend.helper.register("revisioned", revisioned);

hexo.extend.filter.register("stylus:renderer", function (style) {
  style.define("revisioned", (node) => {
    return new style.nodes.String(revisioned(node.val));
  });
});

const calcFileHash = async (filePath) => {
  const buffer = await stream2buffer(hexo.route.get(filePath));
  return hasha(buffer, { algorithm: "sha1" }).substring(0, 8);
};

async function replaceRevisionPlaceholder() {
  const options = hexo.config.new_revision || {};
  const include = options.include || [];
  const enable = !!options.enable || false;

  if (!enable) {
    return false;
  }

  const hashPromiseMap = {};
  const hashMap = {};
  const doHash = (filePath) =>
    calcFileHash(filePath).then((hash) => {
      hashMap[filePath] = hash;
    });

  await Promise.all(
    hexo.route.list().map(async (path) => {
      const [, , extension] = parseFilePath(path);
      if (![".css", ".js", ".html"].includes(extension)) {
        return;
      }

      let fileContent = await readFileAsString(path);

      const regexp = /\.!!revision:([^\)]+?)!!/g;
      const matchResult = [...fileContent.matchAll(regexp)];
      if (matchResult.length) {
        const hashTaskList = [];

        // 异步获取文件 hash
        matchResult.forEach((group) => {
          const filePath = group[1];
          if (!(filePath in hashPromiseMap)) {
            hashPromiseMap[filePath] = doHash(filePath);
          }
          hashTaskList.push(hashPromiseMap[filePath]);
        });

        // 等待全部 hash 完成
        await Promise.all(hashTaskList);

        // 替换 placeholder
        fileContent = fileContent.replace(regexp, function (match, filePath) {
          if (!(filePath in hashMap)) {
            throw new Error("file hash not computed");
          }
          return "." + hashMap[filePath];
        });

        hexo.route.set(path, fileContent);
      }
    })
  );

  await Promise.all(
    hexo.route.list().map(async (path) => {
      for (let i = 0, len = include.length; i < len; i++) {
        if (minimatch(path, include[i])) {
          return doHash(path);
        }
      }
    })
  );

  await Promise.all(
    Object.keys(hashMap).map(async (filePath) => {
      hexo.route.set(
        getRevisionedFilePath(filePath, hashMap[filePath]),
        await readFileAsBuffer(filePath)
      );
      hexo.route.remove(filePath);
    })
  );
}

hexo.extend.filter.register("after_generate", replaceRevisionPlaceholder);

基于 IntersectionObserver 的按需加载

Hexo 博客中一些进行内容渲染的 JS 脚本不是在页面加载时必须立即执行的(比如用于渲染评论区的 JS),除了通过上述方法避免阻塞页面渲染以外,也可以在访客即将看到它之前才开始加载,即按需加载。这需要用到 IntersectionObserver API

在调用 IntersectionObserver API 之前首先要处理一下兼容性问题,避免浏览器不支持 IntersectionObserver API 导致页面内容不显示。然后创建 IntersectionObserver 监听元素出现在视口中的事件。当元素被访客看到时,才进行对应 JS 的加载、执行。下面是代码的实现:

代码语言:txt
AI代码解释
复制
function loadComment() {
  // 插入 script 标签
}

if ('IntersectionObserver' in window) {
  const observer = new IntersectionObserver(function (entries) {
    // 浏览器视口与监听的元素有交集时会触发该回调
    if (entries[0].isIntersecting) {
      // 触发 JS 加载
      loadComment();
      // 取消监听,避免重复触发这个回调
      observer.disconnect();
    }
  }, {
    // 回调触发的阈值,这里是 10% 的部分出现在屏幕中时会触发以上的回调
    threshold: [0.1],
  });
  observer.observe(document.getElementById('comment'));
} else {
  // 浏览器不支持 IntersectionObserver,立即触发 JS 加载
  loadComment();
}

字体裁剪

前面提到我的博客通过 Google Fonts 引入了字体,具体引入的是中文字体 Noto Serif SC(思源宋体)用于标题字体的展示。这里要先说明一下 Google Fonts 对于中文等大字符集的在线字体的提供方式。如果我们通过完整的字体文件向访客分发中文字体是很不现实的,因为一个完整的中文字体包括上千甚至上万个字符,也就是说字体文件的尺寸起码是 MB 级别的,一个字体文件完整下载下来的耗时会很长很长。但是当浏览器支持 font-familyunicode-range 配置后,这个问题就有了转机。

unicode-range 的引入使得我们可以指示浏览器只对特定字符使用特定的字体。比如,以下样式指示浏览器:MyLogo 这个 font-family 只对“电”(U+7535)、“脑”(U+8111)、“星”(U+661F)、“人”(U+4EBA)四个字生效。Google Fonts 将字体切分为多个文件,浏览器在渲染页面时按需下载对应的字体文件,而不是将全部字体文件都下载下来。

代码语言:txt
AI代码解释
复制
@font-face {
  font-family: 'Noto Serif SC';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("./font/logo.woff2") format("woff2");
  unicode-range: U+7535,U+8111,U+661F,U+4EBA;
}

很不巧的是 Google Fonts 提供的字体文件里面,我的首页标题的四个大字刚好就分别分布在四个字体文件上。所以有没有办法把它们合在一起?有的有的,上代码:

代码语言:txt
AI代码解释
复制
const fontCarrier = require('font-carrier');
const transFont = fontCarrier.transfer('./NotoSerifSC-Regular.otf');
transFont.min('电脑星人');
transFont.output({
  path: './logo'
});

这里用到了 font-carrier 库。我们可以只将页面需要用到的文字从完整的字体文件中裁剪出来,生成字体的子集(subset),从而优化字体的加载和展示体验。(目前只在博客的页面大标题上面用了,暂时没有拉取全部文章标题来生成文章页标题的字体文件)

预载下一个页面

最后讲一个比较“取巧”的方法。前面的优化手段针对的是单次页面访问的优化,但访客访问一个站点往往是一个连续的过程,也就是说一位访客进入首页后,如果他对这个网站的内容感兴趣,很有可能通过页面上的超链接继续访问网站的内页。前面已经对 CSS、JS 等静态资源通过缓存优化了加载速度,那么 Hexo 博客的 HTML 静态文件加载是否也有优化的空间?这个问题的回答是肯定的。

这里用到的是 quicklink(https://github.com/GoogleChromeLabs/quicklink),它的实现原理如下:

  • 通过 IntersectionObserver 监听出现在浏览器视口中的 <a> 标签
  • 等待浏览器空闲(通过 requestIdleCallback 注册回调)
  • 向页面插入 <link rel="prefetch" href="a标签指向的 URL">(这会指示浏览器请求该 URL,从而缓存 URL 指向的资源)

这样,在访客点击超链接跳转到博客的内页之前,这个页面的 HTML、CSS 和 JS 文件应该都已经在浏览器的缓存里面了,页面跳转时的网络请求时间开销被极大降低,从而进一步加快了下一个页面的加载速度。

prefetch 预载下一个页面:

页面跳转时使用了 prefetch 的缓存:

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【腾讯云前端性能优化大赛】前端首屏性能优化
网站的性能一直是前端工程师努力的方向之一,更加流畅的体验,更加快速的页面呈现,都是好的web网站的指标之一。
godlanbo
2021/12/27
1.6K0
前端性能优化总结
生产环境中,不需要打印日志。通过对webpack进行配置,打包时自动去掉console.log
前端进击者
2021/07/27
6320
前端性能优化系列 | 加载优化
在浏览器发起网络请求时,并非每个字节都具有相同的优先级,所以,浏览器通常会对所要加载的内容进行推测,将相对重要的信息先呈现给用户。比如浏览器一般会先加载CSS,再去加载JavaScript脚本和图像文件。当然,浏览器的判断并不一定都是准确的,下面就来看看如何影响浏览器对资源加载的优先级。
用户6256742
2024/08/01
1290
前端性能优化系列 | 加载优化
2020前端性能优化清单(二)
2015 年,Google推出了[2]Brotli[3],这是一种全新的开源无损数据格式,并被现在所有现代浏览器支持[4]。实际上,Brotli 似乎比 Gzip 和 Deflate更有效[5]得多[6]。取决于您的设置,压缩可能会(非常)缓慢,但是这样慢的压缩速度最终也会带来较高的压缩率。不管怎样,它都能快速解压缩。这篇文章可以估算站点使用 Brotli 压缩可以节省的大小[7]。
WecTeam
2020/04/28
1.9K0
2020前端性能优化清单(二)
前端性能优化
本来想写一篇实用而又全面的性能优化文章,很多大佬已经写了非常好,我就不再造轮子了。我这篇文章就归纳整理一下吧,方便后续我或他人学习使用。
Dawnzhang
2022/05/10
1.3K0
前端性能优化
cnblogs——从主题开发浅谈前端性能优化
cnblogs-theme是我当前使用的主题,主题基于BNDong开源的进行魔改,但是这为什么会说到性能优化呢?那必然是页面加载存在缓慢的问题呗;
思索
2024/08/15
990
cnblogs——从主题开发浅谈前端性能优化
Web页面全链路性能优化指南
性能优化不单指优化一个页面的打开速度,在开发环境将一个项目的启动时间缩短使开发体验更好也属于性能优化,大文件上传时为其添加分片上传、断点续传也属于性能优化。在项目开发以及用户使用的过程中,能够让任何一个链路快一点,都可以被叫做性能优化。
唐志远
2022/10/27
1.8K0
Web页面全链路性能优化指南
你必须懂的前端性能优化
对于 DNS 解析和 TCP 连接两个步骤,我们前端可以做的努力非常有限。相比之下,HTTP 连接这一层面的优化才是我们网络优化的核心。
张炳
2019/08/02
6880
你必须懂的前端性能优化
前端性能优化方案
前端资源比较庞大,包括HTML、CSS、JavaScript、Image、Flash、Media、Font、Doc等等,前端优化相对比较复杂,对于各种资源的优化都有不同的方式,按粒度大致可以分为两类,第一类是页面级别的优化,例如减小HTTP请求数、脚本的无阻塞加载、内联脚本的位置优化等,第二类则是代码级别的优化,例如JavaScript中的DOM操作优化、图片优化以及HTML结构优化等等。在用户角度前端优化可以让页面加载得更快,对用户的操作响应得更及时,能够给用户提供更为友好的体验,在服务商角度前端优化能够减少页面请求数,减小请求所占带宽,能够节省服务器资源。
WindRunnerMax
2020/08/27
2.7K0
2020前端性能优化清单(五)
当用户请求一个页面时,浏览器获取 HTML 构造 DOM,获取 CSS 构造 CSSOM,然后通过匹配 DOM 和 CSSOM 生成一个渲染树。只要需要解析 JavaScript 时,浏览器就会延迟开始渲染页面的时间。作为开发人员,我们必须明确地告诉浏览器立即开始渲染页面。可以通过给脚本添加 HTML 中的 defer 和 async 属性。
WecTeam
2020/05/06
2K0
2020前端性能优化清单(五)
桌面端前端性能优化策略
例如同一个域名 CDN 服务器上的 a.js,b.js,c.js 就可以按如下方式在一个请求中下载:
laixiangran
2018/07/25
2K0
“非主流”的纯前端性能优化
性能优化一直是前端研究的主要课题之一,因为不仅直接影响用户体验,对于商业性公司,网页性能的优劣更关乎流量变现效率的高低。例如 DoubleClick by Google 发现:
2020labs小助手
2020/09/23
5550
前端性能优化总结
最近花了一些时间在项目的性能优化上,背后做了很多工作,但是最后依然没有达到自己想要的结果,有些失望,但是还是记录下自己的执着。
前端迷
2020/07/02
1.3K0
Web前端性能优化思路
本文旨在整理常见Web前端性能优化的思路,可供前端开发参考。因为力求精简,限于篇幅,所以并未详述具体实施方案。 基于现代Web前端框架的应用,其原理是通过浏览器向服务器发送网络请求,获取必要的index.html和打包好的JS、CSS等资源,在浏览器内执行JS,动态获取数据并渲染页面,从而将结果呈现给用户。 在这个过程中,有两个步骤可能较为耗时,一个是网络资源的加载,另一个是浏览器内代码执行和DOM渲染。 而耗时的增加会导致页面响应慢,卡顿,影响用户体验。 针对上述两种耗时的情况,常见的优化方向有: 缩短
ThoughtWorks
2022/03/04
1.6K0
快速了解前端性能优化
在前端开发的过程中,很多时候除了日常的需求开发以外,我们还需要对我们的页面进行性能优化,那么这次就分享一下前端开发我能想到的一些优化方案进行总结。
LamHo
2022/09/26
9280
快速了解前端性能优化
【腾讯云前端性能优化大赛】微信小程序首屏耗时优化,减少等待降低耗能
RUM 是腾讯提供的一款前端监控方案,只需根据赛事指引在控制台上创建业务系统和应用,获取上报ID;通过安装 npm 依赖配置 JSON 就可以实现测速和日志的收集。
小白伪全栈
2021/12/16
2.2K2
【腾讯云前端性能优化大赛】微信小程序首屏耗时优化,减少等待降低耗能
前端性能优化(PC版)
前端的性能优化是一个很宽泛的概念,最终目的都是为了提升用户体验,改善页面性能。面试的时候经常会遇到问谈谈性能优化的手段,这个我分几大部分来概述,具体细节需要自己再针对性的去搜索,只是提供一个索引(太多了写不过来+主要是懒得写)。这里PC端和移动端分开说了,业务场景不同,需要考虑各自的优化手段
红目香薰
2022/11/29
8940
【腾讯云前端性能优化大赛】前端首屏性能优化实战
在现在的网络环境下,用户访问网页时,如果首屏在3S以内是可以接受的,但是如果首屏在10S以上,绝大部分用户都不会继续等待,这样就会导致用户的流失,对于个人或者企业来说都是不可接受的,所以首屏优化已经成为网页必不可少的一部分。
xwj
2021/12/16
1.6K0
【优化】356- 你必须懂的前端性能优化
对于 DNS 解析和 TCP 连接两个步骤,我们前端可以做的努力非常有限。相比之下,HTTP 连接这一层面的优化才是我们网络优化的核心。
pingan8787
2019/09/24
6070
【优化】356- 你必须懂的前端性能优化
前端性能优化指南
发现总结性的小干货可以为大家提升更好的开发技巧和编码思维,对代码量产化提供更扎实的质量和支持。这次我们来聊聊大家可能都比较关心的话题:「性能优化」。
JowayYoung
2020/04/01
1.3K0
相关推荐
【腾讯云前端性能优化大赛】前端首屏性能优化
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文