波浪线 ~/ 一种优雅的 JS import 路径简化写法

本文对提升您的编程开发能力毫无帮助。仅供代码洁癖患者食用。

在大型前端项目中,用文件夹来组织代码很常见。但是 JS 的引用是基于相对路径的,一旦路径到达一定深度,就会出现非常长的路径:

import Button from '../../../../components/button';
import Factory from '../../../../types/Factory';

这样会带来两个烦恼:

  1. 要精确的计算 ../.. 的层级数量是一项很耗精力的事情。
  2. 如果相对路径很长,就会折行,让代码更难阅读。
继续阅读 →

AI大语言模型上手

OpenAI 的 ChatGPT 开放测试引发了新一波的 AI 热潮。首先 ChatGPT 3.5 在对话能力上达到了令人惊艳的水平。其次,有上亿人通过不同方式亲自体验了这一服务。(即使不对中国用户开放,仍然有数百万人通过 VPN 或代理用上了)

在国外,有 Google 赶鸭子上架的 Bard(上架当天股价暴跌 8%),有 Facebook 被迫开源的 LLaMA(不知道被谁给泄漏了源码)。在国内,百度的文心一言已经开过发布会,阿里的通义千问刚刚开始邀请测试。各家互联网公司都在烧钱搞大语言模型,盈利前景尚不明朗,唯有 NVIDIA 老黄赚的盆满钵满。

在坊间,AI 已经被过度神化,主流媒体甚至也在鼓噪“你的工作会被 AI 取代”这种论调。但是对于科技从业者而言,我们需要冷静看待,AI 大语言模型仍然有很大的局限性。它还远不能取代人,甚至不能取代同为程序的搜索引擎。但是不可否认,就像现在每个人都要使用计算机和手机一样,未来每个人都要使用 AI 做一些事情。了解和接触大语言模型还是很有必要的。

TL;DR

  • OpenAI 的 GPT-4,还没上手。
  • OpenAI 的 ChatGPT 3.5,综合表现在我上手的里面最好。
  • 清华大学的 ChatGLM-6B,基本的中英文对话还行,不能处理复杂逻辑问题,编程能力较差。
  • Google 的 Bard,英文对话接近 ChatGPT 3.5,编程能力较差。
  • Facebook 的 LLaMA,用了但是无法评判。公司自建服务器效果很糟糕,可能不是 65B 完整版。
  • 百度的文心一言,还没上手。在 waiting list 等。但还是给李彦宏点赞,敢吃螃蟹,开放测试。
  • 阿里云的通义千问,还没上手。只对特定的企业用户和媒体开放,这格局真的不敢恭维。

开放程度对比

  • LLaMA 开源,代码和模型都开放下载。虽然用 PC 游戏显卡就能跑,但效果属实不理想,无法称之为对话,更像是鸡同鸭讲。
  • ChatGLM 开源,代码和模型都开放下载。清华大学开发,支持中英双语。唯一一个由非商业公司开发的模型。有基本的对话能力,但是由于参数规模较小,还是比 ChatGPT 3.5 弱很多,尤其是在逻辑,编程,多模态等方面。
  • ChatGPT 3.5/GPT4 闭源,公开测试。中国用户需要 VPN + 外国手机号,或者使用一些代理服务。
  • Bard 闭源,通过 waiting list 加入测试。中国用户只需要 VPN,不需要外国手机号。等待大概半天就能进入。
  • 文心一言闭源,通过 waiting list 加入测试。这个等待时间就比较长了,少则一天,多则几天。
  • 通义千问闭源,通过邀请码加入测试。有测试资格的人极少。阿里巴巴员工目前也不能内测最新版。

功能体验对比

英文对话能力

GPT-4 还没有上手过,未知。据其他人的测试结果,GPT-4 相比 ChatGPT 3.5 增强了一些悖论和数理逻辑的能力,同时强化了伦理道德约束。

ChatGPT 3.5 算是一个基准标杆。

Bard 在英文对话能力上接近 ChatGPT 3.5。

LLaMA 对话能力比较差,经常不能理解问题。

文心一言,还没上手。

通义千问,内测版差强人意,最新版还未上手。

中文对话能力

GPT-4 还没有上手过,未知。

ChatGPT 3.5 算是一个基准标杆。

LLaMA 对话能力比较差,经常不能理解问题。

文心一言,还没上手。

通义千问,内测版差强人意,最新版还未上手。

LLaMA 中文对话能力极其有限,经常胡言乱语。

Bard 目前完全不支持中文。

多模态生成能力

GPT4 能生成代码,图片(基于 OpenAI 自有的技术 DALL·E 2),甚至还能做简单的网页。可以说是目前最强的。

ChatGPT 3.5 能生成代码。

文心一言能生成代码和图片。图片生成基于开源的 Stable Diffusion, 缺点是单纯通过文字 prompt 很难获得比较高质量的图片。Stable Diffusion 最擅长的还是各种第三方模型,LoRA,ControlNet 以及 image-to-image 等组合使用获取高质量图片。然而这些并不能在文心一言中使用。另外 Stable Diffusion 只支持英文输入,文心一言内部是通过将中文翻译成英文实现的,可能会生成完全不相关的图片。(比如总线变公交车)

Bard,ChatGLM 和通义千问目前只能输出文字和代码。

LLaMA 似乎只能进行文字对话,无法输出代码能复杂内容。

图形编程能力

大语言模型很擅长给出各种算法的代码,但是图形编程能力依然很差。我测了一个很简单的编程问题:用 HTML 和 CSS 画一个红十字/create a red cross with HTML and CSS。

ChatGPT 3.5 的回答,错了一半,是十字但是颜色是反的:

<div class="red-cross"></div>
.red-cross {
  width: 50px;
  height: 50px;
  background-color: #f00;
  position: relative;
}

.red-cross::before,
.red-cross::after {
  content: "";
  width: 25px;
  height: 5px;
  background-color: #fff;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.red-cross::before {
  transform: translate(-50%, -50%) rotate(45deg);
}

.red-cross::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

Bard 的回答完全错误,形状都不对:

<div class="red-cross">
  <div class="cross-bar"></div>
  <div class="cross-bar"></div>
</div>
.red-cross {
  width: 100px;
  height: 100px;
  background-color: red;
  border-radius: 50%;
}

.cross-bar {
  width: 50px;
  height: 100px;
  position: absolute;
  top: 0;
  left: 25px;
  background-color: white;
  border-radius: 50%;
}

ChatGLM 的回答也完全错误:

<div class="cross"></div>
.cross {
width: 20px;
height: 20px;
background-color: red;
position: relative;
margin-right: 10px;
}

.cross:before,
.cross:after {
content: "";
position: absolute;
right: 0;
bottom: 0;
width: 10px;
height: 20px;
background-color: red;
border-radius: 50% 50% 0 0;
transform: rotate(-45deg);
}

.cross:after {
right: 10px;
}

当前的局限性

不擅长细分领域的事实性问题

著名华裔科幻作家姜峯楠对此有一个很著名的论断——ChatGPT 就是给万维网(Web)拍了一张很糊的 JPEG 照片。这指出了 ChatGPT 最核心的弱点:它有海量数据,却无法理解深层的联系,容易产生错误的理解;它很能说会道,却不知道自己在说什么,经常编造看似可信的答案。

我想到了中国社会上行走了上千年的江湖骗子。他们没有真才实学,却不论说什么都能头头是道。大语言模型和江湖骗子具有非常多的相似之处。

很多江湖骗子家里满满一书架的书,但都是浅尝辄止并没有深入研究,行走江湖全靠经验阅历。大预言模型也是一样,虽然被喂了大量的数据,但是它阅读理解的效率极低,只有最常见的知识能够训练的比较好。一旦被问及细分领域的问题,由于训练样本不足,它很容易搞不清楚领域的边界。

首先,我们问一个简单的开放性问题:推荐一些漫画?

  1. 《进击的巨人》(Attack on Titan):讲述了人类与巨人之间的战斗,揭示了一个充满阴谋和秘密的世界。
  2. 《海贼王》(One Piece):讲述了主角路飞和他的伙伴们在寻找传说中的宝藏时的冒险故事。
  3. 《火影忍者》(Naruto):讲述了主角漩涡鸣人成为忍者,并成为村庄英雄的故事,充满战斗、友情、爱情和成长的元素。
  4. 《银魂》(Gintama):以幽默搞笑的方式讲述了一个充满科幻元素的江户时代,主角坂田银时和他的伙伴们经历了种种奇怪的事件。
  5. 《攻壳机动队》(Ghost in the Shell):讲述了一个宏大的未来世界和以及人类与机器之间的融合,主角草薙素子是一个拥有高超能力的狙击手。
  6. 《名侦探柯南》(Detective Conan):讲述了高中生侦探小兰和他的朋友变成了童话样子后,以侦破各种案件为题材的故事。
  7. 《鬼灭之刃》(Demon Slayer):讲述了主人公炭治郎为了报仇,踏上了消灭鬼族的旅程。 8.《妖狐×僕SS》(Inu × Boku SS):讲述了一个拥有异能的贵族少女和她的护卫之间的故事,充满了奇幻和浪漫的因素。
  8. 《约定的梦幻岛》(Promised Neverland):讲述了一群孤儿在追求自由的过程中,发现了惊人的真相。充满了惊悚和悬疑的元素。
  9. 《你好,旧时光》:讲述了一个年轻人回到了过去,重新度过青春期的故事,充满了温情和成长的元素。
ChatGPT 3.5

从整体看,这个回答貌似不错,推荐的都是知名作品。细看能发现一些明显的事实性错误,“高中深侦探小兰”是什么鬼!

下一个问题我们再把领域收窄:火影忍者中有哪些主要角色?

  1. うずまきナルト (Uzumaki Naruto):故事的主角,一名忍者,是四代目火影的儿子,拥有九尾妖狐的力量,梦想是成为火影,得到全村人的认可。
  2. うちはサスケ (Uchiha Sasuke):另一名主角,是宇智波一族的后代,对于家族被灭的仇恨深重,梦想是报仇雪恨,但在成长过程中逐渐变得理性冷静。
  3. 春野サクラ (Haruno Sakura):女主角之一,是一名医疗忍者,对于同班同学的爱慕之情逐渐转变为对于团队和保护村子的责任感。
  4. はたけカカシ (Hatake Kakashi):火影之一,是一名传说中的忍者,拥有众多技能和能力,作为三人小队的领队,传授了许多技能给三位主角。
  5. 獣神サスケ (Jūgo Sasuke):一名受到实验变异的忍者,曾经是八尾人柱力的宿主,后成为守护叶村的一员。
  6. 日向ヒナタ (Hyuga Hinata):女主角之一,是一名拥有白眼的忍者,天生有着强大的能力,曾经对于同班同学的爱慕之情逐渐转变为对于团队的责任感。
  7. 綱手 (Tsunade):火影之一,是一名医疗忍者和扑克大师,曾经是三人小队的领队,拥有强大的能力和技能。
ChatGPT 3.5

如果只看前四行回答,是非常不错的。但是后三行回答都有明显的事实性错误:重吾的名字写错了,他也不是八尾人柱力;雏田的描述和春野樱完全重复;纲手缝赌必输,完全算不上扑克大师。总体来看,回答仍然在火影忍者的领域内。

下面我们继续收缩领域:火影忍者晓组织的成员有哪些?

  1. 佐助 (Uchiha Sasuke):在某些情况下,佐助曾是晓组织的一员。
  2. 蝎 (Sasori):拥有木偶的能力,曾经是晓组织的高级成员。
  3. 大蛇丸 (Orochimaru):曾经是晓组织的创始人之一,拥有着强大的血继限界能力。
  4. 鬼鲛 (Kisame Hoshigaki):拥有着一身鲨鱼样的外表,是晓组织的高级成员之一。
  5. 雏田 (Karin):拥有着特殊的医疗能力,是晓组织的成员之一。
  6. 夕日红 (Yūra):是一个熟练的忍者医生,曾经是晓组织的成员。
  7. 御手洗红豆 (Guren):是晓组织的成员,拥有着控制水晶的能力。
  8. 草轮梦蝶 (Tsurugi Misumi):是晓组织的成员,拥有伸缩自如的身体。
  9. 土之器 (Tsuchi Kin):是晓组织的成员,拥有使用声音的能力。
ChatGPT 3.5

可以看到,GPT 回答的错误比例进一步增大到 2/3。当你问的问题越细,GPT 能依靠的数据样本就越少,理解错误就越多。

大语言模型依然很昂贵

OpenAI 据称训练一次 ChatGPT 要耗费数百万美元,每日运营成本也高达十万美元。大语言模型仍然需要大量算力,而核心硬件 GPU/TPU 已经没有了摩尔定律加持。过去十年 GPU 的算力/美元提升了4倍,但是未来十年这样的事情不太可能重演:芯片工艺遭遇物理瓶颈,中美科技战导致供应链成本上升,芯片可能会越来越贵,越来越缺。如果软件算法没有革命性突破,大语言模型将一直是个烧钱的游戏,只有巨头能玩的游戏。另一方面,训练所需的海量数据也需要钱,甚至有钱都不一定能够买到。大语言模型一开始就高筑围墙,可能会进一步加强科技巨头的垄断地位。

现在 OpenAI 也推出了付费的 GPT Plus 服务,20 美元/月。(同时 API 接口还要额外收费)是不是像极了凭几句不靠谱的话术就能赚钱的江湖骗子?现实中也有听风水大师买股票,喝中药汤治疗癌症的。GPT 起码还有一些道德约束,不会给你投资建议或者健康建议。

如果说 OpenAI 这种初创公司烧不起钱还能理解。现在连微软也烧不起了,要给 New Bing 的聊天功能加广告了。这些成本最终还是会转嫁到消费者身上。

前端构建工具 2023

标准

这里定义的前端构建工具需要符合以下条件:

  • 能够构建生产用的前端资源。
  • 提供带 HMR 的 Dev Server。
  • 支持常见的前端技术,比如 Sass, LESS, CSS modules, code splitting。
  • 支持插件扩展。
  • 不限于某种 UI 框架,不捆绑不必要的运行时内容,比如 Redux。(排除 UMI 之类的深度定制框架)

生产可用

这里定义的生产可用条件:

  • 流行度高。(npm 周下载量 >100k)
  • 维护活跃。(六个月内至少发布了一个版本。)
  • 没有严重的性能或兼容性问题。

Vite 4

Vite 基于 esbuild (用 Go 语言编写) 和 Rollup。它采用了 Unbundled Dev Server 模式,通过浏览器原生 ESM 特性,省去了 Bundle 步骤,以达到比 Webpack Dev Server 更快的 HMR 和启动速度。Vite 4 提供了一个新的基于 SWC 的 React 插件 @vitejs/plugin-react-swc ,用于替换旧的基于 Babel 的插件。这让 React Fast Refresh 的速度快了 20 倍。目前,Vite 仍然是速度最快的前端构建解决方案之一。

Vite 易于使用。即使不进行任何配置,也能开始使用。如果需要定制,它的文档也非常详细好用。Vite 兼容 Rollup 插件和配置接口,甚至可以借用 Rollup 的文档。用户更容易找到需要的插件,遇到问题也更容易找到解决方案。编写 Vite 插件也相对容易。所以 Vite 的社群和生态系统成长很快。Vite 的 JS API 设计简单清晰,功能丰富。基于 Vite 开发你自己的构建工具很容易。

Parcel 2

Parcel 2 基于一系列用 Rust 重写的高性能模块,比如 SWC 和 Parcel CSS,实现了优于 Webpack 的性能。默认配置下,Parcel 2 的性能介于 Vite 4 和 Webpack 5 之间。但是可以通过启用一些实验性功能提升性能,比如 swc 压缩。

Parcel 2 的另外一个特点是开箱即用。它的默认配置已经涵盖了 TypeScript,CSS modules,JSX,React Fast Refresh,图片压缩,SVG 优化等特性支持。另外,当检测到 Sass 或 LESS 时,Parcel 2 会自动帮你安装需要的依赖包。对于常规的前端应用,你完全不需要配置文件。

Parcel 有自己独特的架构和接口设计,这让它几乎不可能直接借用 Webpack 和 Rollup 的插件。因此它的生态系统中第三方插件很少。另外,由于 Parcel 发布大版本的频率很低,内置插件为了避免 Breaking Change 而很难升级,比如 MDX 2.0 至今没有被支持。这可能是 Parcel 2 最大的劣势。

Webpack 5

Webpack 依然是最流行的选择,有最完善的特性,以及最大的生态系统。

Webpack 很慢,但是你可以用一些 loader 和插件替换默认组件来提速:

观察列表

一些还处于早期阶段的有潜力项目。

Rspack

用 Rust 写的 Webpack 替代品,由字节跳动创建。虽然还处于早期阶段,但是已经表现出比 Vite 更快的构建速度。同时兼容 Webpack 插件,可扩展性很强。

未入选

Turbopack

为 Next.js 框架设计的高性能构建工具,原生支持 SSR。未入选原因是因为目前只能用于 Next.js 框架。

UMI

蚂蚁集团的构建工具。未入选原因是因为过度定制化,只支持 React,内置了过多 Redux 之类的运行时。

ICE

阿里巴巴的构建工具。未入选原因是因为过度定制化,只支持 React,内置了过多 Redux 之类的运行时。

Create React App

React 官方的构建工具。未入选的原因是因为过度定制化,只支持 React,不支持配置和插件。

性能测试

dev server cold startdev server warm startproduction build
rspack 0.1.12.2s2.0s1.4s
vite 4.2.14.3s1.3s6.5s
parcel 2.8.313.0s1.3s15.7s
webpack 5.76.214.2s14.2s15.8s

详情参见 https://github.com/guoyunhe/front-end-builder-benchmark

技术趋势

一些可能会让前端构建工具更快更好的技术。

原生 Node.js 模块与 WASM 模块

用 Rust,Go 等语言编写的 Node.js 模块,其性能与产物格式有很大的关系。不考虑多线程的情况,从高到低分别是 Native > WASM > JS。JS 和 WASM 本身都是单线程的,除非使用 Cluster 模式(目前还没有在前端构建工具上比较成功的)。支持多线程的 Native 模块,性能会更强,esbuild 就是一个例子。

但是,原生模块必须为不同的平台预编译对应的二进制文件,不如 WASM 或 JS 的“一次编译,到处运行”方便。

在实践中,原生模块通常用于核心高负载模块,比如 rspack,swc,esbuild。而插件还是广泛采用 JS。WASM 仍然比较少见,一个例子是 swc 的插件开发。

从 Unbundled 回到 Bundled?

用 JS 写的 Webpack Dev Server 采用 Bundled 模式启动。即先由 Webpack 将多个源码文件打包成一个,再传输给浏览器。在浏览器的网络记录中,你只会看到几个网络请求。因为 Webpack 打包的效率低,启动速度很慢,热加载也很慢。

后来出来了 Snowpack (已废弃),可能是第一个 Unbundled 模式的 Dev Server。采用 Unbundled 模式启动 Dev Server,通过原生 ESM 加载,省去了 Bundle 的过程,提高了启动速度。当 Dev Server 启动时,不会进行编译,而是在接到浏览器请求时,动态编译每个文件,每次传输一个源文件对应的代码。过程大致是:

请求 → 编译 → 响应 → 请求 → 编译 → 响应 → … → 请求 → 编译 → 响应

这导致 Snowpack 的启动速度极其缓慢,对比 Webpack Dev Server 并没有显著优势。

后来 Vite 继承并改进了 Snowpack 的思路。它将 node_modules 中大量的依赖用 esbuild 先 Bundle 成单个文件,而项目源码依旧采用 Unbundled 模式。得益于 esbuild 的强大性能,不管是依赖的 Bundle,还是单个源码文件的转译,都非常快。因此 Vite 对比 Webpack Dev Server 取得了巨大的性能优势。

然而,随着项目体积增大,需要加载的 ESM 模块越来越多,Vite 的启动速度会变慢。假设项目的 src 中有 1000 个 JS/TS 文件,就会有 1000 个 HTTP 请求。现代的浏览器只允许每个服务器建立 6 个连接,也就是说最多只能并发 6 个 HTTP 请求,导致阻塞。

因此 Parcel 2,Turbopack 和 Rspack 选择了回到 Webpack Dev Server 的 Bundled 模式,通过用 Rust 重写关键部件,改进打包和编译性能来提高性能。

但是根据 Turbopack 的 Benchmark,只有在 src 中有 1000 个以上的 React 组件时,Turbopack 对比 Vite 才会有比较明显的优势。但实际应用中,1000 个组件(约100K到1M行代码)已经是相当大规模的应用了。而 Vite 的冷启动速度仍然可以接受:4.2s。另外,我觉得这个 Benchmark 用 Turobopack 的 Server Side Rendering 对比 Vite 的 Client Side Rendering,并不公平,因为 Vite 也是支持 Server Side Rendering 的。

总结一下,我觉得 Vite 的 Unbundled 策略和 Parcel 等的 Bundled 策略各有所长,在实际应用中并不能拉开明显的性能差距,未来很长一段时间仍然会共存,而不是此消彼长。

更快的 Sass 编译器

Sass 的老编译器 node-sass 已经被官方废弃,不再维护。新的官方编译器 Dart Sass 令人迷惑:

  • 原生构建二进制程序没有 Node.js 接口,无法集成到现有的 Node.js 工具中。除非你用 Node.js 的 exec 函数执行。(不推荐在生产中这样做)
  • JavaScript 构建要比 node-sass 慢 4 到 30 倍 (取决于项目大小)。如果你的构建工具调用 Sass 接口的方式不正确,速度甚至会更慢。详见 Issue #1557#1534

对于大量使用 Sass 的生产项目,这肯定是不可接受的。很多项目选择继续使用废弃的 node-sass 编译器。

现在已经有人在开发用 Rust 重写的 Sass 编译器:

  • grass。只提供编译成 WASM 的版本,但是性能并不好。
  • rsass。可惜,这个项目也没支持 Node.js API,也无法直接用于现在的前端构建工具生态。

更快的 TypeScript 检查器

TypeScript 很慢,因为它完全是 JavaScript 写的。幸运的是,我们通常不需要在构建前端项目的时候检查类型。Babel, esbuild 或 swc 会直接忽略所有类型。但是,如果你项目有很严格的 QA 要求,你不得不运行 tsc 命令检查类型,CI 工作流就会被拖慢,即使对于小型项目 (10k lines) 或中型项目 (100k lines) 也很明显。

于是乎,重写 TypeScript 检查器的呼声越来越高。我们已经能看到一些这方面的尝试:

浏览器兼容性与 Polyfill 技术

开发者通常在最新的浏览器上进行开发,以使用最新的 JavaScript 语法,浏览器接口,让工作更有效率,代码质量更高,更不容易过时。

而用户则可能仍然在使用旧版的浏览器。比如,某些政府教育系统仍然在使用 Windows XP,用户能安装的最新的浏览器是 Chrome 49。有些用户可能只是没有升级浏览器的习惯,导致他们的 Chrome 还是 86 版本。

为了平衡两方面,开发者通常会借助一些工具,自动转译语法,并 Polyfill 新的接口,以兼容某个特定版本的旧浏览器。通过语法分析,按需插入 core-js 中的 Polyfill 代码。而 core-js 缺少的部分,可以用 polyfill-service 补全。

ES 语法转译

Optional Chaining 这种比较新的 ES 语法在老版本浏览器无法支持,运行时会报错。

const dogName = adventurer.dog?.name;

为了兼容旧版浏览器,必须在构建过程中就将其转译成 ES5/ES3 语法。

var _adventurer$dog;
const dogName = (_adventurer$dog = adventurer.dog) === null || _adventurer$dog === void 0 ? void 0 : _adventurer$dog.name;

转译功能通常由 babel,swc,esbuild 等 JavaScript 转译工具提供。

ES 特性 Polyfill

Promise,Stream 等特性需要插入特定的 Polyfill 代码。

esbuild 无法自动分析并 Polyfill 特性,只能手动导入。而 swc 和 babel 可以自动分析源码,按需自动导入。swc 和 babel 都是基于 core-js 进行 polyfill。

浏览器特性 Polyfill

core-js 只提供 ES 特性的 Polyfill(准确的说,是浏览器和 Node.js 交集的部分)。但是浏览器除了 ES 标准外,还提供其他浏览器特有的特性,比如 ResizeObserver。这就需要一个补充方案。

方案1是手动取导入 Polyfill 代码,比如在项目中 import 'whatwg-fetch' 就可以支持 fetch() 接口。但是这对开发者而言是一个比较大的负担。

方案2是在页面中引用 polyfill.io 的脚本,服务器会根据 UA 判断需要加载哪些 Polyfill 代码。开发者也可以用 polyfill-service 自建服务器。缺点是增加了一些服务器开销,也会阻塞应用本体脚本的加载,造成一些成本提高和性能损失。

CSS 语法转译

很多 CSS 特性在未成为正式标准之前便已经被浏览器支持了。但是在这个成为标准的过程中,CSS 特性的提案可能会修改。浏览器厂商为了防止自己目前的实现与之后的正式标准冲突,会在选择器和属性名之前,加上自己的前缀,Firefox 是 -moz-,Chrome 和 Safari 是 -webkit-,而 IE 是 -ms-。

比如下面这段代码:

:fullscreen {
}

为了兼容不同的浏览器,尤其是老浏览器,需要转译成:

:-webkit-full-screen {
}
:-ms-fullscreen {
}
:fullscreen {
}

转译过程主要通过 postcss 实现。当前流行的前端应用构建工具,比如 create-react-app, vite, parcel, 都集成了 postcss。

横向对比

babelesbuildswcpolyfill servicepostcss
ES 语法转译
ES 特性 Polyfill
浏览器特性 Polyfill
CSS 语法转译

总结

综合来看,目前没有一种技术能够单独达成全自动的浏览器兼容性。建议使用 babel + polyfill service + postcss 之类的融合方案。

当使用一种新特性时,可以先查一下 caniuse.com 支持的浏览器版本。如果需要 Polyfill,要看自己选用的 Polyfill 方案(core-js,polyfill-service)是否支持。如果不支持,则需要手动 Polyfill 这个特性。

实践

Create React App

以 create-react-app 项目为例。首先我们需要配置项目支持的浏览器版本,让底层的 babel 和 postcss 将 JS 和 CSS 转译到目标浏览器支持的语法,并尽可能注入所需的 Polyfill 代码:

{
  "browserslist": {
    "production": [
      "chrome > 49",
      "firefox > 53",
      "ie 11"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

然后,我们需要修改 HTML 模板,注入 Polyfill.io 的脚本,以支持额外的 Polyfill 特性:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://polyfill.io/v3/polyfill.min.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

Vite

Vite 默认只支持 Chrome 87+,这个要求对大部分实际应用可能太过苛刻。如果要支持更早的浏览器,则必须使用 legacy 插件:

import react from '@vitejs/plugin-react';
import legacy from '@vitejs/plugin-legacy';
import { defineConfig } from 'vite';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    legacy({
      targets: [
        "chrome > 49",
        "firefox > 53",
        "ie 11"
      ],
    }),
  ],
});

然后,我们需要修改 HTML 模板,注入 Polyfill.io 的脚本,以支持额外的 Polyfill 特性:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://polyfill.io/v3/polyfill.min.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

扒一扒深圳外企的前端/全栈招聘

最近找工作接触过的一些深圳外企。

Artflow AI(远程)

职位:全栈。

来源:猎头。

流程:

  • HR 直接拉了一个群,初创公司,CEO 和 CTO 都是华人。
  • 技术笔试,做一个全栈 demo, 重视单元测试。
  • 技术面试,中文,两个面试官分别是 CTO (算法/架构)和全栈开发。问的都是一些实际项目的问题,没有很偏门很细节的问题。
  • 综合面试,英文,两个面试官分别是 CEO 和另外一个 Co-Founder。主要问项目经历,以及对这个公司做的产品有什么想法和建议。
  • 谈薪资待遇(最后没谈拢)。

优点:

  • 不用八股文,不用刷题。
  • 流程很快,从递简历到拿 offer 一周时间。
  • 全员远程,灵活办公。
  • 可以谈 14 薪,比 16 薪或 18 薪要保险一点。

缺点:

  • 试用期三个月,发正式工资的 80%。
  • 公积金 7%。
  • 因为时差的原因,开会时间是中国的晚上 9 点到 11 点。

SkyScanner

职位:全栈。

来源:LinkedIn。

流程:

  • HR 电话。
  • 待续。

亚马逊 AWS

职位:前端架构(偏全栈)。

来源:LinkedIn。

流程:

  • HR 电话,没啥特别的。
  • 技术面试1,主要是介绍自己做过的项目,不会问技术细节问题。
  • 技术笔试,做一个全栈的系统 demo (技术栈不限)。要用到至少一种 AWS 服务,最常用的是 S3,要实际部署到 AWS 上运行,还要写系统架构设计报告,比较耗时。我觉得他们的岗位描述有问题,笔试全是全栈+运维的东西,前端的比例很少。
  • 技术面试2,要求做 PPT 介绍笔试题目的系统设计。我到这一步就被通知招聘冻结了,没有后续了。

优点:

  • 不用八股文。
  • 不用刷题。
  • 薪资水平在外企中算高的。

缺点:

  • 做 demo 比较耗时,前后搞了一周的时间。主要是 AWS 比较难搞,之前的工作都是有专门的运维搞AWS, 开发是不会直接去操作的。
  • 突然终止招聘不太靠谱啊,可能很长一段时间都没岗位了。
  • 开发也要求走客户,出差可能会比较多。

Flexport

职位:全栈。

来源:公司官网。

流程:

  • HR 电话,聊完之后说安排面试但又没有后续了。

缺点:

  • HR 不靠谱。
  • 薪资的现金部分比阿里低,期权妥妥的废纸。

Nike

职位:全栈。

来源:脉脉。

流程:

  • HR 面试,有英文口语部分。
  • 技术笔试,做一个全栈 demo, 技术栈不限,重点要求单元测试和 CI (GitLab)。
  • 技术面试,有英文口语部分,不涉及笔试 demo 的内容,也没有问技术细节,主要介绍项目经历和个人发展的问题。
  • HR 沟通职级和薪资,因为降薪太多,就没继续了。

优点:

  • 不用八股文。
  • 不用刷题。
  • 面试过程比较轻松友好。

缺点:

  • 薪资比较低。
  • 要对接 Nike 收购的数据公司,可能要处理一堆技术债务。

避坑

  • 国内小厂:不要相信小而美。
  • 假外企,假国企:都是大厂出来的,比大厂还能卷。

总结

我个人是比较喜欢偏实践的面试/笔试的,毕竟啥 service worker 啥 event loop 的技术细节,工作中也是随用随搜,工作中 99% 用不到。面试的大部分都是全栈或者前端偏全栈,在做 demo 的过程中我会尝试一些新的技术,比如 Vitest, Adonis, 还是很有收获的,比每天上班重复那一套东西要有趣。

技术上,纯前端不多,大部分是全栈。前端 React 还是比较通吃,有些公司用 Vue,不建议学 Angular 了。后端 Node.js, Python, Java 比较多,但是面试很多不限制编程语言,可以后面再转。除了基本的功能实现,单元测试基本上每家都会考一下。另外 CI, 云服务,也是常会遇到的。笔试考 demo 的好处是可以边做边学,不用事先背一大堆可能用不到的东西。

外企的薪资普遍比较低。薪资高的要么是亚马逊这样的大厂,要么是搞区块链的不差韭菜。阿里的开发 P6 英文叫 Software Engineer II, 对应到其他外企同级别的岗位一般也叫这个,再上面是 Senior Software Engineer 和 Lead Software Engineer。但是普通外企的 Software Engineer II 的年薪也就是阿里开发 P5 的水平,甚至 Nike 这种 Senior Software Engineer 也给不到阿里 P6 的水平。

但是另一方面,大部分外企的评价体系决定了他们是真的闲。有项目就做项目,没项目就自行安排。不会像国内大厂一样,盲目上项目堆人力,做十个烂九个。开会没完没了,对接数不清的人。国内大厂的小领导们,带着下属疯狂卷影响力,对公司业务和个人成长基本是负作用。这种又累又绝望的感觉,让我觉得工资低一些的外企也变香了。

继续找。

下一代前端构建工具

2022 年,Webpack + Babel + Terser 仍然是前端项目构建工具的主流,广泛用于各种生产环境。不可否认,Webpack 和 Babel 仍然在持续进化,对各类研发提供了最稳定且全面的支持。但是随着前端项目规模的增长,CI 流水线的普及,构建性能问题已经成为了影响开发效率的一个关键因素。下一代构建工具,试图从不同的角度实现对构建效率的提升。

esbuild

用 Go 语言实现的构建工具,提供了编译,打包,压缩能力。由于是原生程序,且几乎不依赖第三方库,充分利用 CPU 多核架构等原因,esbuild 实现了夸张的毫秒级编译速度,效率数百倍于 Webpack + Babel。

优点:

  • 高性能。目前最快的实现,整体上略优于 swc。
  • 低占用。Go 语言对字符串的内存优化很好,即使采用多线程工作方式,内存占用仍然远低于基于 Webpack 的工具。

缺点:

  • 自身不提供 HMR,需要依赖其他工具实现。
  • 不支持 React Refresh,对 React 的 HMR 不能用 webpack-dev-server + esbuild 编译的方案。
  • 产物代码分割功能仍处在实验阶段,相比 Webpack 和 Rollup 还是比较简单,不太可靠。
  • 插件生态不健全。

适用场景:

  • 纯 JS 库构建。不需要 HMR,只需要 watch 能力。
  • Node.js 后端应用构建。不需要 HMR,只需要 watch 能力。
  • Jest 和 SSR 等 Node 运行环境的代码转译。
  • 嵌入其他构建工具。比如 Vite 用了 esbuild 的编译和压缩能力,不足的部分用 Rollup 插件体系和自己实现的 HMR 补全。

swc

用 Rust 语言实现的构建工具,swc 实现了编译和压缩能力,swcpack 实现了打包能力(实验性)。swc 设计之初就是完全对标 Babel 的,相比 esbuild 拥有更广泛的适用场景。

优点:

  • 高性能。实际测试略慢于 esbuild,但也比 webpack+babel+terser 快上几十倍。
  • 支持 React Refresh。这一点是对 esbuild 的巨大优势,且短期内 esbuild 无法克服的问题。

缺点:

  • 自身不提供 HMR,需要依赖其他工具实现。
  • 打包能力还是实验性质,缺乏高级配置支持。
  • 插件生态不健全,用 Rust 编写插件门槛较高。

适用场景:

  • 纯 JS 库构建。不需要 HMR,只需要 watch 能力。
  • Node.js 后端应用构建。不需要 HMR,只需要 watch 能力。
  • Jest (@swc/jest) 和 SSR 等 Node 运行环境的代码转译。
  • 嵌入其他构建工具。比如 Parcel 用了 swc 的编译和压缩能力,不足的部分用自己实现的 HMR 补全,并有 Parcel 自己的插件体系。

Vite

基于 esbuild 的编译和压缩能力,Bundleless 的 HMR 实现,并用 Rollup 插件生态补足了常用的打包特性。目前性能和特性综合考量的最佳选择。

更早的 Bundleless 实现 Snowpack 已经泯然众人了。Bundleless 最大的问题在于,现在前端应用的依赖树太深太广,模块文件太多,逐个文件加载导致启动速度极其缓慢。而 Vite 则是融合了 Bundle 和 Bundleless 两种策略,对 node_modules 还是用 esbuild 去快速 bundle,一次性加载,对于源码再采用 bundleless 策略。这样就平衡了启动速度和热加载速度。

优点:

  • 构建速度更快
  • 热更新速度更快
  • 能够使用众多的 Rollup 插件

缺点:

  • 由于 esbuild 不支持 React Refresh,React 项目还是要走 Babel,一定程度上拖慢了速度。
  • Rollup 及其插件生态还是以 JS 为主,性能优化空间有限。Vite 作者寄希望于 esbuild 打包能力和插件生态成熟,用 esbuild 替换掉 Rollup。

适用场景:

  • Web 前端应用
  • 浏览器插件
  • Electron 应用
  • 小程序

Parcel 2

Parcel 基于 swc 并有自己的 HMR 和插件生态。设计上与 Vite 类似,但 Parcel 自研部分更多,而 Vite 依赖且受限于 Rollup。性能上 Parcel 甚至更快一些,尤其是缓存性能,热启动极快。但在稳定性上 Parcel 控制的并不好,一个最简单的 React 应用都需要一些 hack 才能工作。

优点:

  • 构建速度快
  • 缓存效率高
  • 热更新速度快

缺点:

  • 不兼容 pnpm
  • 插件生态不足

适用场景:

  • Web 前端应用
  • 浏览器插件
  • Electron 应用
  • 小程序

Turbopack(Alpha)

用 Rust 实现 Webpack 替代,工作原理和 Webpack + Webpack Dev Server 保持一致,而没有走时下流行的 bundleless 路线。当项目源码(不包括 node_modules)文件数量达到数千,Vite 启动速度会随文件数量降低,即使本地几乎不存在网络延迟,数以千计的 HTTP 请求以及代码转译任务仍然耗时不菲。而 Turbopack 则通过提升打包性能,在保持 Webpack 工作方式的情况下,实现了快速启动和 HMR。当前 Turbopack 的测试性能是领先 Vite 的。

假如 Vite/Parcel 也用 Rust/Go 完全重写,那么 Vite/Parcel 和 Turbopack 应该具有以下关系:

  • 在文件数量小于 N 时,Vite/Parcel 的冷启动速度更快,但是 Turbopack 也足够快。
  • 在文件数量大于 N 时,Turbopack 的冷启动速度更快,且于 Vite/Parcel 的差距越来越大。
  • Vite/Parcel 的 HMR 速度总是快于 Turbopack,但是两者都足够快,以至于差异肉眼无法分辨。

可惜 Turbopack 目前并没有推出正式版本,完全没有达到 Vite/Parcel 的成熟度,因此完全没有可比性。目前只能关注,深入分析或评测意义不大。

JavaScript 十进制库选型

众所周知,计算机以二进制存储数字,而真实世界中用十进制表示数字。二进制和十进制的差异可以用一个乍看非常怪异的例子说明:

> 0.1 + 0.2
0.30000000000000004

不管是 JavaScript, Python 还是 Java,都能够得到类似的结果。

分析原因,就像十进制中的 1/3 是无限循环小数,二进制中的 1/10 和 1/5 也是无限循环小数。计算机的基本数字类型都是有精度限制的。两个无限循环小数相加,导致精度损失,产生了 0.30000000000000004 这样的结果。

在很多情况下,这个并不会有实际影响。因为十进制同样有精度误差,十进制并不比二进制更精确。它真正导致的问题是违反直觉。对于人直接可见的数字计算,比如购物车结算总价,出现二进制精度问题显然会让不了解二进制原理的普通用户感到诧异。

另外,原生浮点数的取整方法都是四舍五入。按照统计学规律,大量累加起来,总数会偏大。对于电商,银行和支付系统,会导致交易的某一方长期多付出,是违反公平的。因此这些系统常常采用银行家算法(四舍六入五成双)。

最后,整数和浮点数都有固定的精度限制,且受限于硬件或编译器无法更改。计算超出范围的大数会出错,不安全。

基于这三点原因,在电商和金融系统中,会使用十进制类而非基本数据类型来计算和存储交易数字。

JS 常用库比较

JavaScript 目前仍然没有原生的十进制类。我们只能从一些提供十进制类的 NPM 包中选择。不同的十进制库提供的功能不完全相同。通常功能越多体积越大。目前开发比较活跃且比较流行的 JS 库都来自同一位作者。差异详见此处

NPM 包big.jsbignumber.jsdecimal.js
打包尺寸 Minified + Gzipped2.9kB8.1kB12.3kB
精度控制N位小数N位小数N位有效数字
除法精度损失有损有损有损
加、减、乘法精度损失无损无损有损
超大数支持,计算较慢支持,计算较快支持,计算较快
超小数支持,计算较慢支持,计算较慢支持,计算较快
舍入模式ROUND_DOWN
ROUND_HALF_UP
ROUND_HALF_EVEN
ROUND_UP
ROUND_UP
ROUND_DOWN
ROUND_CEIL
ROUND_FLOOR
ROUND_HALF_UP
ROUND_HALF_DOWN
ROUND_HALF_EVEN
ROUND_HALF_CEIL
ROUND_HALF_FLOOR
ROUND_UP
ROUND_DOWN
ROUND_CEIL
ROUND_FLOOR
ROUND_HALF_UP
ROUND_HALF_DOWN
ROUND_HALF_EVEN
ROUND_HALF_CEIL
ROUND_HALF_FLOOR
EUCLID
指数计算支持支持支持
对数计算不支持不支持支持
复数计算不支持不支持支持
三角函数计算不支持不支持支持

从简单到复杂排列:big.js < bignumber.js < decimal.js

从打包体积来看,big.js 最小,在浏览器端占据优势。

在电商和金融场景,更常见的精度控制方法是控制 N 位小数,big.js 和 bignumber.js 更加合适。而在科学计算和工程领域,可能 decimal.js 的控制 N 位有效数字的特性会更合适一些。

除法都是有精度损失的,这是无法避免的。但是 decimal.js 的加减乘法也有精度损失,这一点需要注意。

big.js 对于超大数的计算会比 bignumber.js 或 decimal.js 慢,要结合具体场景分析是否会性能瓶颈。在前端/客户端场景,或者小型电商后端,计算量很小,且大数不常见,那么 big.js 完全可以胜任。在大型电商平台和金融交易后端,大数计算量繁重,那么 bignumber.js 会更好一些。科学和工程领域需要的超大数计算,则 decimal.js 会更合适。(但是这些领域通常也不会用 JS 这么慢的语言,所以 decimal.js 的定位有点尴尬)

超小数通常只在金融,科学和工程领域出现,这种数据的计算 decimal.js 会比 big.js 和 bignumber.js 快。和上面的大数应用场景分析类似,要视情况选择。

默认的舍入模式都是四舍五入(ROUND_HALF_UP),且都支持银行家算法——四舍六入五成双(ROUND_HALF_EVEN)。对于大多数电商和金融场景都足够用了。

decimal.js 还支持对数,复数,三角函数等高级数学运算。

按应用场景选择

中小型电商

中小型电商的交易数字都偏小,big.js 在前后端都可以胜任,满足精度控制,舍入模式和性能的需求。同时 big.js 的体积较小,有利于优化前端传输速度。

大型电商平台和金融系统

前端/客户端通常只处理单一客户的计算需求,大数出现频率较少,通常不存在性能问题。 big.js 依然可以胜任。

后端则可能会频繁处理大数,这时候 bignumber.js 会更快一些。但是,这种情况通常表明整个系统都面临性能问题,更加实际的做法是将重度计算的服务用 Java 之类的更高性能语言重构……

科学计算和工程领域

decimal.js 功能更加丰富,对极大和极小数计算的性能更好,比 big.js 和 bignumber.js 更合适。

但是,这些领域通常对性能要求很高,用 JS 的情况非常少见。建议直接使用一种更合适的语言……

总结

前端选择 big.js 一般都没错。它简单小巧,却又能提供所有我们需要的功能。

后端 Node.js 不需要考虑打包尺寸,如果有比较多的大数计算量,也可以上 bignumber.js 的。

decimal.js 的定位则比较尴尬,除非你特别需要某些科学计算功能,否则没有必要选它。

实践

  1. 阿里巴巴 Fusion Design 从 bignumber.js 切换到 big.js PR 链接

前端最佳实践:字体

字体单位

px 依旧是最全面的选择,尤其是重交互的网站:

  1. 能够做到设计稿的像素级还原。
  2. 用户体验统一,变量少,可控。
  3. 方便配合 JS 动态计算布局,JS 接口通常只能获取像素为单位的尺寸和位置。
  4. icon,图片和 border 通常都是使用像素尺寸,字体采用像素单位更容易配合。

rem 在文字内容型网站上更加灵活,适合新闻,博客等:

  1. 自适应不同设备,不同用户偏好,获得最佳阅读体验。
  2. 无需手写繁复的 media query。

有些创意内容网站使用 vw 尺寸系统,字体和图片总是同窗口比例缩放,以实现类似海报布局的整体感,但也仅限于这一类型的网站。

em 和 % 会用在局部使用,但存在嵌套后尺寸不容易控制的问题,极少在项目中大范围使用。

pt 是印刷常用单位,不适合现如今的 Web 环境,且和 px 对应关系复杂,应该避免。

正文字体大小

12px 是 Chrome 浏览器默认支持显示的最小字体。即使 CSS 设置了 9px,最终用户看到的仍然是 12px 字体。因此在设计中使用小于 12px 的字体是一个严重的错误。12px 的英文字体可读性尚可,但是中文可读性比较差。建议作为次要文本字体的尺寸。

13px 是信息密度较高,中文可读性尚可的选择。比如 Facebook 和百度用的就是 13px。

14px 是信息密度和中文可读性的一个比较好的平衡点,适合界面复杂且空间比较紧张的网站的正文字体大小。比如

16px 是大部分浏览器的默认字体大小,可读性好,但是信息密度不高,适合一般网站。

18px 以上通常只有在创意营销网站上才会作为正文字体大小使用。

一个比较通用的策略是:

  • 12px:次要文字,页脚链接,标签分类,面包屑,输入框提示,小按钮,小输入框
  • 14px:普通文字,正文,输入框,按钮
  • 16px:突出文字,卡片标题,大按钮,搜索栏

标题层级字体大小

HTML 支持 H1 到 H6 共六级标题。但实际应用中,我们应当控制标题的层级数量,层级越多,使用体验会越差。

功能为主的网站对标题依赖比较小,更多是靠 Card 和 Tab 等容器进行界面层级管理。

内容型网站对标题层级的需求会更高一些,但也尽量不超过三级。

H1 字体大小取决于网站类型对信息密度的要求。以功能为主的系统通常 H1 会比较小,不会超过 40px。

H1, H2, H3 之间应当有 30% 左右的递减,视觉上差异才够显著。比如:

  • H1, 36px:页面标题
  • H2, 24px:章节标题
  • H3, 16px:子章节标题,卡片标题,弹窗标题

字重

最早字体只支持两种字重,正常(normal, 400)和粗体(bold, 700)。现代 Web 字体的字重支持 100 到 900 等 9 种字重,100 最细,900 最粗。但是系统字体支持的字重数量通常较少。

Windows 默认中文字体“雅黑”支持 300, 400 和 700 三种字重。macOS 和 iOS 默认中文字体“苹方”支持 100/200/300/400/500/600 六种字重(苹果更喜欢使用较细的字体设计)。也就是说“苹方”的粗体,会比“雅黑”要细一些。Android 系统默认字体 Noto Sans CJK 支持 200/300/400/500/700/900 六种字重,最为全面。部分 Android 系统,比如魅族 Flyme 和小米 MIUI,甚至搞出了字重的无极调节。

由于中文字体体量巨大,通常网站不会加载远端中文字体,而是使用系统字体。因此,在使用字重的时候,就需要考虑系统字体的字重支持。综合主流操作系统,得出兼容性最好的字重集合:细体 300,正常 400,粗体 700/600

变更日志 Changelog 的书写方法

变更日志(Changelog)是软件工程中的一种常用手段,用于记录每个软件版本所发生的变更。一方面,可以方便下游及时兼容;另一方面,让协作开发者和用户了解项目进展。变更日志需要打版本号,但并不一定要遵循 Semantic Versioning。

最简单的形式

  1. 推荐使用 Markdown 语法,文件名为 CHANGELOG.md。
  2. 用二级标题标记版本号,建议标注发布日期,以便日后回溯。合并主干分支但尚未发布的,用“未发布”标题,放在开头。
  3. 对于不兼容的变化,用“破坏性变更”标记。
# 变更日志

## 未发布

- 修复 Button

## 2.0.0 - 2021-09-17

- **破坏性变更**: Table 组件重命名为 Grid

## 1.1.0 - 2021-03-21

- Card 组件支持 size (small/large/medium)
- 新增 List 和 Table 组件

## 1.0.0 - 2021-02-08

- 新增 Card 组件
- 新增 Button 组件

增加模块标记

对于模块比较多的库或应用,变更项目会很多,不容易阅读。比较好的方式是在开头标记这个变更属于哪个模块,不属于任何模块的用 Other 标记。比如说你的应用有很多页面,则可以按页面来划分。如果是组件库,则可以按组件来分。如果是单个复杂组件,则可以按属性或功能来分。

## 2.1.0 - 2021-09-17

- Card: 增加 size (small/large/medium) 属性
- List: 新组件
- Button: 修复 loading 样式
- Input: 增加 warning 图标
- Other: 修复 IE 11 兼容性

细分变更类型

实践中,分类越详细,开发者维护变更日志的工作量越大。不分类是完全可行的,只有当你有充足的时间和很高的追求的时候,再考虑。

有很多种方法可以给变更分类。

按特性(Feature)和修复(Fix)分类是最简单可行的,也更符合当下软件工程的模型,容易配合各种项目管理工具。最重要的是,通常没有什么争议。

## 2.1.0 - 2021-09-17

### 特性

- Card: 增加 size (small/large/medium) 属性
- List: 新组件
- Input: 增加 warning 图标

### 修复

- Button: 修复 loading 样式
- Other: 修复 IE 11 兼容性

如果你听说过 Keep a Changelog ,它提供的分类方法要复杂得多。

## 1.0.0 - 2022-02-02

### 新增

- List: 新组件
- Input: 增加 warning 图标

### 更改

- Card: large size 的 padding 从 12px 调整为 16px

### 移除

- Card: align 属性,用 headerAlign 和 footerAlign 替代

### 废弃

- Input: focus 属性,建议用 Ref 

### 修复

- Button: 修复 loading 样式
- Other: 修复 IE 11 兼容性

对大部分项目来说,这样做意义不大,会消耗开发者更多的精力。因此建议变更不做细分,或者按照上面特性+修复的简单分类方法。

为什么不用 Git 历史

大多数项目的 Git 历史 commit message 含义不清,很难去阅读。

为什么不用项目管理系统

JIRA 和 AONE 之类的项目管理系统,或者 NPM 这样的软件包平台,确实能够在一定程度上记录软件发布历史。但实际项目中,会出现不得不从一个平台迁移到另一个平台的情况。这些发布记录有时候并不能随代码迁移到新平台。另外,软件分发给客户或者下游之后,他们并不一定能访问我们内部系统的发布历史。

React 页面加载速度优化(非SSR)

React 应用页面首次加载通常比后端渲染静态页面慢,这对用户体验有着极大的负面影响,尤其是移动端。服务器端渲染(SSR)是非常有效的手段,但实现成本过高,不止要调整技术架构,也会增加服务器负载。那么在不使用 SSR 的前提下,如何优化 React 应用的首屏加载速度呢?

加载过程分析

使用 Firefox 开发者工具网络面板对页面加载过程中的网络流量进行监控,是最常用的分析方法。

注意右侧的进度瀑布图有两根线条,一根红色,一根蓝色:

  • 蓝色是 DOMContentLoaded 事件触发的时间点
  • 红色是 load 事件触发的时间点

但是对于 React 应用而言,以上两个事件并不标志着页面内容已加载完成。React 应用通常会需要请求一些 JSON 接口(比如商品信息),然后再加载 UI 组件(比如商品列表)。实际加载时间,我们可以通过 performance.now() 接口在组件实际挂载(componentDidMount 或 useEffect)时获取:

import React, { useEffect, useState } from 'react';

function App() {
  const [products, setProducts] = useState([]);
  useEffect(() => {
    fetch('api/products').then(res => res.json()).then(setProducts);
  }, []);
  if (products?.length > 0) {
    return <ProductList products={products} />
  } else {
    return <div>Loading...</div>;
  }
}

function ProductList({ products }) {
  useEffect(() => {
    console.log('ProductList loaded in ' + performance.now()/1000 + 's');
  }, []);
  return products.map(({name}) => <div>{name}</div>);
}

本地的测试分析不能反映实际用户的加载速度。建议将加载时长结果上报到服务器,进行统计分析,制作按日/小时的趋势图,以及按加载秒数的分布图。统计结果将有助于判断后续的优化工作的效用有多大。

数据加载优化

首屏展示依赖的数据,后端直接注入到 HTML 中,从而减少网络请求数量。

<script>
  window.user = {{ JSON.stringify(user) }};
  window.productInfo = {{ JSON.stringify(productInfo) }};
  window.translations = {{ JSON.stringify(translations) }};
</script>
<script src="path/to/js"></script>

要小心控制注入 HTML 中的数据数量,过大的 HTML 可能会导致页面加载失败的概率增加。首屏不展示的数据,或者不重要的数据要排除:Select 的数据源,弹窗中用到的数据,通知数据等。翻译数据最好按首屏需要做一次切分,且只透出当前用户语言。

从实践经验来看,这一项措施,对加载速度提升最为明显。

图标加载优化

icon font 是图标加载方式的主流,但加载字体文件会多发出一个请求。且字体请求通常是在 CSS 中触发,会阻塞页面加载。另外,字体文件很难根据首屏内容进行切分,不够灵活。某些大型应用,图标高达上百个,加载数 MB 的图标字体会让体验变得很差。

更好的方案是用 SVG Sprite 直接嵌入 HTML 中,或用 Inline SVG 直接嵌入 JS 中,减少一次网络请求,同时只加载首屏需要的图标。

打包尺寸优化

首先,建议所有使用 Webpack 4 的项目迁移到基于 Webpack 5 或者 Rollup 的构建工具,以使用 ES6 Module 的特性进行 Tree Shaking,去掉未被使用的部分,缩减产物尺寸。

对于 lodash 这种非 esm 包,建议用 lodash-es 替代或者采用分量引用:

// Bad
import _ from 'lodash';

// Good
import get from 'lodash/get';

最后,使用 Webpack Bundle Analyzer 插件分析产物尺寸,识别对产物体积影响较大的包。

  1. 存在多版本的包,考虑升级依赖版本进行优化。
  2. 尺寸过大的包,考虑轻量级替代品,比如 moment -> date-fns/day.js

代码分割

注意:不支持 IE,Edge < 16,Firefox < 60,Chrome < 61 和 Safari < 11

常见的构建工具都支持以下语法进行代码分割:

import React from 'react';
import Header from './Header';
const Description = React.lazy(() => import('./Description'));
const Comments = React.lazy(() => import('./Comments'));
const FeedbackDialog = React.lazy(() => import('./FeedbackDialog'));
const Footer = React.lazy(() => import('./Footer'));

function ProductDetailPage() {
  return <div>
    <Header/>
    <Description/>
    <Comments/>
    <FeedbackDialog/>
    <Footer/>
  </div>
}

对于首屏需要展示的组件,Header,我们用传统的引用方式打包,在页面加载时直接加载其 JS 和 CSS。而对于不会首屏展示的组件,用 ES6 模块动态加载。这样做之后,首屏加载的 CSS 和 JS 尺寸会缩小,从而加快首屏展示。

参考 React 官方文档

但实际项目中,构成项目产物的大部分来自第三方包,比如 React,Moment 和 Ant Design 这样的库,也就是 vendor chunk。如果你的业务代码产物 app chunk 在 gzip 压缩之后不超过 200KB,做代码分割的意义就不大了。

总结

在实际工作中,由于技术架构限制或是资源限制无法使用 SSR,我们还是有很多手段可以去快速优化 React 应用加载速度。

本文没有涉及 HTTP2,缓存策略,CDN 等优化传输速度的方法,因为这些是 DevOps 考虑的范畴,在大的 IT 公司中,都已经做得比较到位了,前端开发者不需要亲历亲为。