现如今,在呈现一个页面时,在浏览器中会打开众多进程,包括浏览器、渲染、插件、GPU、网络等进程。
  浏览器进程负责存储、界面、下载等管理。在渲染进程中,运行着熟知的主线程、合成线程、JavaScript 解释器、排版引擎等。
  而呈现一个页面大致可分为 4 个步骤:
1. 浏览器进程处理用户在地址栏的输入,然后将 URL 发送给网络进程。
2. 网络进程发送 URL 请求,在接收到响应数据后进行解析,接着转发给浏览器进程。
3. 浏览器进程收到响应后,发送“提交导航”消息到渲染进程。
4. 渲染进程开始接收网络进程发送的数据,并进行文档渲染。
  基于上述步骤可以联想到,呈现的优化分为两部分:资源和渲染。
  像上一节的[图像](https://www.cnblogs.com/strick/p/17080155.html)其实也属于资源部分,只是内容比较多就单独创建了章节。
  本文所用的示例代码已上传至[Github](https://github.com/pwstrick/pe)。
## 一、资源
  HTTP Archive 关于 2022 年页面大小的[报告](https://almanac.httparchive.org/en/2022/page-weight)指出,按大小升序后,排在中间位置的移动页面大概有 70 个请求。
  包括 22 个图像、21 个脚本、7 个 CSS以及 2 个 HTML,脚本和 CSS 占了 40% 的请求。
  除了对这些资源进行尺寸优化之外,还可以对它们的加载进行优化。
**1)优先级**
  浏览器会给不同资源给予不同的请求优先级。
  以 Chrome 为例,分为多个等级,包括 Highest 、High、Low 和 Lowest 等,如下图所示。
:-: ![](https://img.kancloud.cn/0f/1b/0f1bfa58a579fae278c7bf0229fa3b82_1580x606.png =800x)
  HTML 和 head 元素中的 CSS 优先级是最高的,head 元素中的脚本是高优先级,异步请求的脚本是低优先级。
  若优先级不符合预期,可以通过一些配置修改优先级,例如为 script 元素声明 async/defer,它的优先级就会变成低。
  在 img 元素中,新增了一个[fetchPriority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority)属性(如下所示),当值是 high 时,意味着这是一张重要的图像,浏览器会提升优先级立即开始请求。
~~~html
<img src="hero.png" fetchpriority="high" />
~~~
**2)link 元素**
  link 元素常用来加载 CSS 文件,但它还支持些其他功能,接下来会一一介绍。
  当 link 的 rel 属性值为[preload](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload)时,就能预加载资源,如下所示。
~~~html
<link rel="preload" href="demo.js" as="script" />
~~~
  as 属性是告知浏览器加载的资源类型,包括 style、script、font、image 等。
  预加载可提升资源的优先级,不过当资源在几秒后未使用时,浏览器会发出告警。
  2023-11-20 高版本的浏览器已经原生支持 JavaScript 模块化(即 import 语法),这意味着可以在浏览器内直接基于模块编写 JavaScript 而不用编译和打包。
  但是模块依赖项会带来加载问题,因为浏览器需要先等待模块加载,然后才能找到其依赖项。
  从 Chrome 66 开始,rel 属性支持一个新的关键字:[modulepreload](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/modulepreload),解决上述问题。
  它用于声明预加载模块,也就是预加载依赖项,以便浏览器提前知道相关文件。
  当 link 的 rel 属性值为[preconnect](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preconnect)时,就能预连接站点,如下所示。
~~~html
<link rel="preconnect" href="https://www.pwstrick.com" />
~~~
  另一个与连接相关的类型是[dns-prefetch](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/dns-prefetch)(如下所示),用来处理 DNS 查询,即 DNS 预解析。
~~~html
<link rel="dns-prefetch" href="https://www.pwstrick.com" />
~~~
  当 link 的 rel 属性值为[prefetch](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/prefetch)时,就能预提取资源,如下所示。
~~~html
<link rel="prefetch" href="demo.js" />
~~~
  预提取会让资源的优先级降为最低,用于让某些非关键资源提前请求,可为用户的下一步交互做准备。
  2023-03-23 当 link 的 rel 属性值为[prerender](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/prerender)时,就能预渲染指定的网站,如下所示。
~~~html
<link rel="prerender" href="https://www.pwstrick.com" />
~~~
  不过,该参数的兼容性有限,Safari 和 Firefox 都不支持,如下图所示。
:-: ![](https://img.kancloud.cn/48/25/48257a005353368f95810aeeca491d1d_1357x221.png =800x)
  有个名为[Tachyon](https://fasterthanlight.net/)的开源库,基于 prerender,对页面之间的导航进行了提速。
  在用户将鼠标移动到链接时,会通过创建 link 元素,并赋予 prerender,实现指定地址的预渲染。
**3)script 元素**
  延迟(defer)和异步(async)的出现是为了解决 script 元素阻塞 HTML 解析的问题,下图描绘了 script 元素的 3 种运行机制。
:-: ![](https://img.kancloud.cn/f6/4c/f64c9584717884ad8fe052240c5b1e33_1581x245.png)
  第一行是默认的运行机制,在解析HTML文档时,一遇到 script 元素就停止解析,改成下载外部脚本,然后执行脚本,执行完后才会继续解析。
  第二行是使用了 defer 属性后的运行机制,HTML 文档的解析和外部脚本的下载是同时进行的,解析完后才会执行脚本。
  第三行是使用了async 属性后的运行机制,HTML 文档的解析和外部脚本的下载也是同时进行,但下载完后就开始执行脚本,执行完后才会继续解析。
  2023-11-20 有个库叫 [Partytown.js](https://github.com/BuilderIO/partytown),比较有意思,可以将第三方脚本迁移到 Web Worker 中执行,防止阻塞主线程,不过这还只是个测试性的库。
**4)数据预请求**
  在客户端的 WebView 中,每次请求后端接口大概要花 100~200ms,如果把这段时间省下来,那么也能减少白屏时间。
  数据预请求是将请求时机由业务发起提前到用户点击时,并行发送数据请求,缩短数据等待时间,如下图所示。
:-: ![](https://img.kancloud.cn/be/db/bedbd7b2a749a61f9cd1e7ea547f3f09_1274x372.png =600x)
  这种改造需要客户端配合,现在简单介绍下我们公司当时实现的方案,流程图如下所示。
:-: ![](https://img.kancloud.cn/9b/f8/9bf87beac1a3f77c86445fe860c811ac_705x750.png =600x)
  首屏数据的接口信息,可以通过一些配置关联起来,比如一个单独的配置接口。
  客户端在拿到数据后,就会缓存到一个全局变量中,等待脚本读取。
  注意,到底是客户端先拿到数据,还是网页先拿到,这个无法确定,并且预请求只能以 get 方法通信。
  具体的实现方案如下:
* 客户端分析出当前 URL 中的路径和参数,其中 refresh 参数(有的话)是一个时间戳(秒),这个参数用来控制客户端是否需要重新请求配置接口。
* 当分析的 URL 参数中无 refresh 字段时,访问 https://xxx.com/settings 接口,并将URL路径、客户端默认带的参数(包含用户ID等)和 URL 本身的参数全部传递过来(如下所示),然后本地缓存。
~~~
https://xxx.com/settings?path=game%2Fstrick&uid=xxxxx&refresh=1618451992
~~~
* 客户端会将 settings 接口的响应数据缓存到本地,而 key 就是当前 URL,也就是说 URL 不变的话,默认就不会去请求 settings 接口。若要穿透缓存,那么加上 refresh 参数,赋一个与之前不同的值即可。
* settings 接口返回的 JSON 格式,包含 urls 字段(如下所示),是个数组,由接口集合组成,已经拼接好参数。
~~~
{
"urls": [
"http://xxx.com/xx/xx?id=2",
"http://xxx.com/yy/yy?uid=1"
]
}
~~~
* 客户端将读取到的数据注入到 WebView 的全局对象中,可以用全局变量同步读取,名字可自行约定,例如叫 TheLClientResponse,读取方式:window.TheLClientResponse,JSON 格式如下,其中 key 是 api 的路径,如果无数据可以返回 null。
~~~
{
"xx/xx": {
code: 0,
msg: "test",
data: {
list: []
}
},
"yy/yy": {
code: 0,
msg: "test",
data: {
list: []
}
}
}
~~~
**5)字体**
  CSS3 提供了[@font-face](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face)规则允许为网页指定自定义字体,其声明和使用如下所示。
~~~css
@font-face {
font-family: "iconfont";
src: url("../font/iconfont.woff2") format("woff2"),
url("../font/iconfont.woff") format("woff"),
url("../font/iconfont.ttf") format("truetype");
}
.iconfont {
font-family: "iconfont";
}
~~~
  上述字体来源于[iconfont](https://www.iconfont.cn/),为了兼容性考虑,往往会提供多个格式的字体。
  其中 ttf 是一种未压缩的格式,另外两种内部都做过压缩。在 2022 年大概有[75%~78%](https://almanac.httparchive.org/en/2022/fonts#performance)的网页在使用 woff2 格式的字体。
  使用字体除了改变文字外形之外,还有一种普遍用法是用来显示 icon 小图标。
  2023-11-27 因为这样就能让 icon 成为矢量图,所以缩放既不会变形,也不会影响流量。
  CSS3 提供了[font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display)属性用于指定字体的渲染方式,在 @font-face 中声明,2022 年用的最多的值是 swap。
  swap 会让文字先按浏览器默认的字体展示,当字体加载完成后,再将其替换掉。在慢网中,会看到字体的前后变化。
  所以应该尽快加载字体,才能让用户享受到最优的体验。
  浏览器在解析 CSS 文件时,并不会马上下载 @font-face 中的字体文件。
  只有当发现 HTML 中有非空节点使用该字体时,才会开始下载。
  如果要提早下载,那么可以使用预加载,如下所示。
~~~html
<link rel="preload" href="../../assets/font/dakai.woff2" as="font" crossorigin="anonymous"/>
~~~
  crossorigin 属性是必填的,表示允许跨域,若省略,就会有告警。
  还有一种优化方法是提取字体的子集(即有选择性的将需要的字符组合在一起),减小字体文件的尺寸,像图标就比较适合这样自定义。
## 二、渲染过程
  浏览器的渲染过程大致可分为 8 个阶段,如[下图](https://cabulous.medium.com/how-browser-works-part-i-process-and-thread-f63a9111bae9)所示。
:-: ![](https://img.kancloud.cn/8a/44/8a44a4900621fb22fef2b83ec0087153_1752x621.png =600x)
  下面的 1~5 步涉及主线程(main thread),6~8 步涉及合成线程(compositor thread)。
1. 将 HTML 解析成 DOM 树,并将其存储在内存中,同时下载解析到的资源。
2. 将 CSS 解析成样式表(style sheets),即生成 CSSOM,在此阶段会计算节点样式,并把相对的值和单位都转换成像素。
3. 通过 DOM 和样式表生成布局树(layout tree),在此阶段会计算元素的尺寸和坐标,并且在树中不包含隐藏元素,但会包含 CSS 中创建的内容。
4. 对布局树进行分层,生成分层树(layer tree),可控制绘画顺序,裁剪元素内容,CSS 中的 transform、z-index、will-change 等属性都与层相关。
5. 通过布局树和分层树生成绘制列表,并将其提交给合成线程。
6. 通过绘制列表和图层生成图块(tile),因为渲染所有图块会比较昂贵,所以会划分优先级,例如视口中的可见图块优先级会高。
7. 图块在提交到光栅化(raster)线程池后,会被转移到 GPU 中,加速光栅化处理,即转换成位图(bitmap),最终结果会存储在 GPU 内存中。
8. GPU 将位图传送回合成线程后,就会生成合成帧,处理完所有位图后,合成器线程向浏览器发送 Draw Quad 命令,开始在屏幕上显示页面。
  虽然这 8 个阶段的执行过程比较复杂,但是在现代浏览器中,它们会在 1/60 秒(即 16.67 毫秒)内完成,下图描述了整个渲染过程。
:-: ![](https://img.kancloud.cn/21/48/214839522706fd4b654c03153a872a43_1230x1534.png =600x)
  优化渲染过程的核心就是缩短某个阶段的执行时间,或者直接跳过某些阶段。
**1)流式渲染**
  HTTP/1.1 协议支持分块传输编码(chunked transfer encoding),允许服务器将网页数据分成多块后再进行传输。
  在响应头中设置 Transfer-Encoding: chunked 就会启用分块传输编码的响应格式。
  浏览器在知道 HTML 会被流式返回后,就不用等到 HTML 下载完成后再开始解析了。
  不过,目前流行的客户端渲染(Client Side Render)其实并不需要专门的流式渲染,因为 HTML 的内容本来就少。
  若改成服务端渲染(Server Side Render),那就可根据实际情况进行流式渲染的优化了。
  具体的实现过程,本文不再赘述,可参考网上相关的方案,例如 Vue SSR 指南中的[流式渲染](https://v2.ssr.vuejs.org/zh/guide/streaming.html)。
**2)DOM**
  HTML 在被解析时,一旦遇到 JavaScript,那么就会被阻塞,如下图所示。
:-: ![](https://img.kancloud.cn/2c/3c/2c3c8e0e4602d9105487c5098894c368_720x465.png =600x)
  当遇到外部脚本时,还会停止 DOM 树的构建,转由网络进程去请求 JavaScript 脚本地址。
  CSS 本身并不会阻塞 DOM 树的构建,但在与 JavaScript 结合使用时,会出现阻塞。
  在下面的示例中,JavaScript 会修改 demo.css 文件中的样式。
~~~html
<link rel="stylesheet" href="demo.css" />
<div id='root'>内容</div>
<script>
const root = document.getElementById('root');
root.style.color = 'red';
</script>
~~~
  主线程在执行脚本之前,需要先计算节点样式(即解析 CSS 文件),因此 DOM 树就无法被继续构建了。
  若要优化 DOM 树的构建,除了尽量避免上述不科学的写法之外,还可以从两方面入手:减少关键资源请求的数量和大小。
  所谓关键资源(key resource),更确切的说就是网页首屏的核心资源,没有它们,那么首屏将无法正确的呈现。
  减少资源的请求数量可以通过 2 个方法:
* 将 CSS 或 JavaScript 内联到 HTML 结构中,例如移动端的屏幕适配脚本就比较适合内联。
* 脚本元素可以增加 async 或 defer 的标记,具体可以参考上一节的 script 元素。
  关键资源的大小除了进行压缩外,就是只提取首屏需要的代码。
  将其他部分的代码合并到另一个文件,待需要时再加载,或者使用上一节所说的预提取。
**3)重排和重绘**
  重排(reflow)也叫回流,是指修改元素的几何属性后引起的重新渲染,涉及 7 个阶段,如下图所示,修改了元素的高度。
:-: ![](https://img.kancloud.cn/f3/d9/f3d91d50d5423396f9e48d984edc8d3e_2043x826.png =600x)
  触发重排的情况有添加或删除可见的元素、修改位置、边距或内容等。
  重绘(repaint)是指修改元素的背景颜色后引起的重新渲染,但与重排不同,重绘将直接进入 Paint 阶段,如下图所示。
:-: ![](https://img.kancloud.cn/37/58/37580acca37444f23b38f16c63e79199_2041x853.png =600x)
  重排和重绘都会降低渲染性能,因为它们都发生在主线程中,并且布局、分层和绘制 3 个阶段的计算过程比较昂贵。
  当在脚本中获取元素的尺寸、位置等排版相关的信息时,就有可能触发强制重排,例如调用 offsetTop、clientWidth、getComputedStyle() 等属性或方法。
  优化它们的方式包括使用 cssText 或 CSS 类修一次性修改多个 CSS 属性,批量修改 DOM,例如使用文档片段 fragment、先隐藏元素再显示等。
  在众多的 CSS 属性中,有两个 CSS 属性(transform 和 opacity)可以避开重排和重绘,直接进入合成阶段。
  例如用 transform 属性实现的元素变化,就不会占用主线程,而是由合成线程处理,如下图所示。
:-: ![](https://img.kancloud.cn/4c/c5/4cc5a8c5d6f854d63898bf93e7ae17ec_2041x834.png =600x)
  值得一提的是,早期在脚本中实现动画,都会借助定时器,但定时器无法精确的配置动画帧之间的时间间隔。
  按屏幕刷新率为每秒 60 次计算,那么理论上每帧的间隔约等于是 16.67 毫秒。
  但实际情况比较复杂,间隔不一定是这个值,有可能出现丢帧,从而造成动画不够平滑流畅。
  为了解决动画问题,浏览器提供了[requestAnimationFrame()](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame)方法,在每一帧的开始执行配置的回调。
  注意,只有当浏览器 GPU 生成位图和屏幕显示位图保持同步时,才会触发 requestAnimationFrame() 的回调。
  在下面的示例中,让绝对定位的 span 元素通过 requestAnimationFrame() 向右偏移。
~~~html
<span id='container' style="position:absolute">内容</span>
<script>
let left = 0;
const frame = () => {
const container = document.getElementById('container');
container.style.left = `${left++}px`;
if (left > 100) return;
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
</script>
~~~
  注意,requestAnimationFrame() 也是运行在主线程中,如果主线程繁忙,那么也有可能延迟回调,造成动画的卡顿。
  并且如果其回调比较耗时(超过一帧),那么就会阻碍后续的任务。
**4)离屏渲染**
  2023-11-20 离屏渲染相当于开辟一个缓冲区,将屏幕外的内容提前绘制好,在滚动或需要时就能直接呈现对应内容。
  一种实现方式是维护两个 Canvas,当前渲染的 Canvas 与隐藏的缓存 Canvas 两者交替绘制,这不仅复杂,并且还要消耗更多的计算资源。
  另一种是使用 [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) 实现真正的离屏,这是一个实验中的新功能,提升 Canvas 在 2D/3D 绘图时的渲染性能和使用体验。
  与 Canvas 只能在主线程中执行不同,OffscreenCanvas 既可以在主线程中执行,也可以在 Web Worker 中执行,这让不影响主线程的离屏渲染成为可能。
## 总结
  本文的第一章节详细描述了资源的优化,并在开篇指出资源都存在着优先级,浏览器会按优先级进行请求。
  预加载可提升资源的优先级,预提取可降低资源的优先级,预连接可提前进行 TCP 连接或 DNS 查询。
  script 元素有延迟和异步两种运行机制,可有效地防止 HTML 解析的阻塞。
  数据预请求需要与客户端配合,本文给出了一份解决方案可供参考。
  自定义字体在页面开发中有着广泛的应用,常用的优化手段是预加载和减小尺寸。
  在第二章节中详细分析了浏览器的渲染过程,这个过程大致可分为 8 个阶段。
  围绕这些阶段,引出了流式渲染、DOM 树构建的优化。
  在重排和重绘中,详细说明了它们影响的阶段,并且列举了触发原因,以及优化手段。
  最后提到了合成动画,并且对比了 JavaScript 动画的两种实现方式。
*****
> 原文出处:
[博客园-前端性能精进](https://www.cnblogs.com/strick/category/2267607.html)
[知乎专栏-前端性能精进](https://www.zhihu.com/column/c_1610941255021780992)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎阅读。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020