这并不是一篇网络上泛滥的“前端体积优化”文章。
百尺竿头,更进一步!本文以我的博客为例,介绍 极限管制 页面体积的奇技淫巧。
成绩预览
眼见为实,自己博客首页 的网络传输总体积为 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-width
为min-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");