关于javascript:3-KB-的博客首页我是如何做到的

39次阅读

共计 3809 个字符,预计需要花费 10 分钟才能阅读完成。

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

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

成绩预览

眼见为实,自己博客首页 的网络传输总体积为 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");

正文完
 0