这并不是一篇网络上泛滥的“前端体积优化”文章。

百尺竿头,更进一步!本文以我的博客为例,介绍极限管制页面体积的奇技淫巧。

成绩预览

眼见为实,自己博客首页 的网络传输总体积为 2.6 KB

  • 自己的博客 Repo 在 kblog - GitHub,喜爱就给个 Star 呗~

需要精简

平铺直叙的页面,体积再小,也难能可贵。我须要:

  • 单页面(SPA)。
  • 应用 Material Design 质感设计格调。
  • 疾速构建与加载。

没有代码是最好的代码。尽量削减需要,能力基本上减小体积。于是——

  • 仅适配新版浏览器。
  • 仅应用 Markdown 外围语法。
  • 局部遵循 Material Design,舍弃简单个性。
  • 前端与生成器均不应用框架。

打包与压缩

将 CSS、JS 等资源进行打包早已是常识,但我心愿走得更远一些,将所有资源(除页面自身外)合并至单个文件。于是有 bundle.js

let avatar = `/*{avatar}*/`;document.head.insertAdjacentHTML("beforeend", `/*{head}*/`);

其中形似 /*{xxx}*/ 的标记,将被替换为须要嵌入的资源。而嵌入的内容中也可含有标记,一直替换,直至所有资源嵌入实现。

例如,/*{head}*/ 将被替换为 head.html

<link rel="icon" href="${avatar}" /><style>  /*{style}*/</style>

留神到,我在这里将网页图标也嵌入了。但即使你不须要图标,也应指定一个 <link ... href="data:"> 空白图标,否则浏览器将主动向 /favicon.ico 发送多余申请。

要嵌入图像,咱们通常会将其以 Base64 进行编码。但我应用的是 SVG 图标,为文本格式,因此将特殊字符应用 encodeURIComponent() 转换后,就可间接间接写作 data:image/svg+xml,<svg ... </svg>,从而防止 Base64 编码所带来的体积收缩。

切记,引入 bundle.js<script> 标签不应有 defer 属性,且必须在 <head> 中。这与大多数教程的举荐做法南辕北辙,却正是我想要的成果:在嵌入的 CSS 加载实现之前,不要渲染页面。

因为申请数量少,再佐以 HTTP2 的服务端推送,阻塞渲染并不会显著拖慢加载速度。

单页面计划

通常,在动态页面实现 SPA,需别离生成动态页面和 JSON。框架辅助下开箱即用,但有诸多毛病:

  • 响应的 JSON 是未转换的 Markdown,解析导致页面卡顿(可改善)。
  • 首次拜访加载工夫较长(可应用 SSR 解决)。
  • 体积大,构建慢(无解)。

还有一种办法是以 404 页面为路由。易于实现(利用 GitHub API)但首屏加载迟缓,且极不利于 SEO。

而我的博客则抉择了另一条路——

得益于前文的资源打包,页面中有效内容极少(只需引入 bundle.js 即可)。例如,某篇文章生成页面如下:

<title>Hello - kkocdko's blog</title><script src="/bundle.js"></script><main>  <article>    <h1>Hello</h1>    <p>Hello world!</p>  </article></main>

实现页内切换,首先要标记页内链接。个别思路是应用 data-xxx 自定义属性,但在这里,咱们约定:<a> 标签 href 属性以 /. 前缀,即为页内链接,如 <a href="/./hello/">Hello</a>。家喻户晓 . 代表当前目录,因此此做法不会造成行为扭转。

顺便说一句:这种做法的益处,远不止于抠出一些字节,更重要的是,这容许咱们以原生 Markdown 语法在文章内写出页内链接 [对于](/./about/) 而不是突兀的 <a data-spa-link href="/./about/>对于</a>

在链接被点击后,间接 fetch 指标页面,提取内容,更新到当前页面上

onpopstate = () =>  fetch(location) // location.toString() === location.href    .then((res) => res.text())    .then((text) => {      // 有些玄学的解构      [, document.title, , box.innerHTML] = text.split(/<\/?title>|<\/?main>/);    });

赋值给 onpopstate 是为了使得页面在后退、后退时也能更新内容。

再实现一下监听页内链接(每次页面更新后运行):

for (const element of document.querySelectorAll('a[href^="/."]'))  element.onclick = function (event) {    event.preventDefault(); // 防止间接跳转    history.pushState(null, null, this.href); // 更新 URL    onpopstate(); // 因为 "pushState" 不会触发 "popstate" 事件  };

至此,咱们初步实现了单页面反对。

简洁的实现代码

有很多技巧,可能在实现等价性能的前提下,缩小所需的代码量,此处仅举一例。当然,在生产我的项目中应用时需谨慎。

以本博客页面中 <main> 的 CSS 为例。此元素是页面次要内容的容器。须要实现的性能有:

  • 在顶部、底部留白。
  • 一代子元素(卡片)居中,圆角,投影成果,元素间留白。
  • 宽度过低时(挪动端)勾销各处空白、暗影;子元素的间隙改为分隔线。

通常的实现如下,共 452 字符:

main {  display: grid;  grid-gap: 20px;  justify-content: center;  margin-top: 75px;  margin-bottom: 25px;}main > * {  width: 680px;  margin-top: 20px;  border-radius: 8px;  box-shadow: 0 1px 4px #aaaaaa;}@media screen and (max-width: 750px) {  main {    grid-gap: 0;    margin-top: 50px;    margin-bottom: 0;  }  main > * {    width: 100%;    border-bottom: 1px solid #aaa;    border-radius: unset;    box-shadow: none;  }}

这里有很多可优化的位点。

  • @media 查问中 screen and 是不必要的,匹配所有类型并没有太大问题。
  • 有些属性在 @media (max-width ... 中被重置,能够改 max-widthmin-width,再将宽度过低 / 宽度失常的属性调换,省去重置语句。
  • Grid 和 justify-content 是不必要的,咱们能够对 <main> 固定宽度以束缚子元素,再应用 margin: auto 居中。
  • 上一条批改过后,margin 能够与顶部留白 margin-top 缩写,原有的 4 行代码,缩减为单行 margin: 75px auto 25px
  • 子元素间隙用 margin-top 实现。首个子元素的 margin-top 与容器的 margin 重叠,顶部空白放弃失常。
  • 应用 box-shadow 向下偏移 1px 来代替 border-bottom,缩小几个字节,同时省去 @media 块中的重置语句。

利用上述技巧,实现如下:

main {  width: 100%;  min-height: 100vh;  margin: 50px 0 0;}main > * {  margin-top: 1px;  box-shadow: 0 1px #ddd;}@media (min-width: 750px) {  main {    width: 680px;    margin: 75px auto 25px;  }  main > * {    margin-top: 20px;    border-radius: 8px;    box-shadow: 0 1px 4px #aaa;  }}

仅 309 字符,相较原来的 452 字符,缩小了 32%,十分可观。

看得开心么~

这只是自己博客我的项目中所用技巧的一小部分。其余内容,限于篇幅,不再穷举。若你想要深刻理解,请见 kblog - GitHub。

  • 测试用动态服务器代码(举荐应用 mkcert 治理证书):
const serve = require("http2").createSecureServer;const read = require("fs").readFileSync;const load = (p) => require("zlib").brotliCompressSync(read(p));serve({ cert: read("cert.pem"), key: read("cert-key.pem") }, (_, res) => {  res.setHeader("content-type", "text/html;charset=utf8");  res.writeHead(200, { "content-encoding": "br" }).end(load("index.html"));  res.createPushResponse({ ":path": "/bundle.js" }, (_, r) => {    r.writeHead(200, { "content-encoding": "br" }).end(load("bundle.js"));  });}).listen(4000, "127.0.0.1");