console.info
Hello ~ 我是小斑,一个富文本编辑器。今天,咱来聊聊体重,对,没错!就是那令人头疼的体重!任何事物都烦这个体重,当然也包括我,当我的创造者阿飞发布小斑的第一个版本时,小斑足足有 4M
之重!看着控制台茫茫多的流量消耗,他愣住了:你。。。特么的这么怎么胖,足足 4M
(包括 CSS
)!但小斑我也很无奈呀!代码是你写的,怪我咯!
软件发展总归避免不了两个过程:野蛮生长 & 精心优化,小斑长算是长出来了,但这一身肥膘得好好减减!
哝!上图,就是我刚诞生时,所包含的 JS
模块,足足 3M
之多!但小斑的核心代码仅仅存在于右下角蓝色的方块内,其他都属于增强体验的代码,包括 UI
框架,代码高亮,表情的便捷输入等等。
接下来,小斑瘦身记,正式开始。
数据压缩:从 4M 到 1.5M
提到数据压缩,就不得不提一个鼎鼎大名的数据压缩算法:Gzip
。
Gzip
全名: GNU zip
开源的数据压缩算法,广泛应用于网络传输。大家可能会有疑惑,数据压缩算法这么多,凭啥 Gzip
如此出名?因为压缩比高?
no!no!no!仅仅因为它是 GNU
开头,互联网诞生于一个专利横飞的年代,各种好用的压缩算法受限于专利,并不能使用在开放的互联网上,因此大家需要一个高效且开源的压缩算法,Gzip
因此诞生!
使用 Gzip
很简单,过程大概也就 3 步:
- 服务端使用
Gzip
将需要发送的数据进行压缩; - 客户端将接收的数据并进行
Gzip
解压; - 客户端使用解压后的数据;
因此,Gzip
是服务端对输出内容的优化,且需要客户端支持(客户端需要有解压的能力)。
等等!需要客户端支持?会不会很麻烦?
当然不!别忘了 Gzip
的全名是 GNU zip
,它可是开源的!只要客户端内置 Gzip
模块,就可以完全使用 Gzip
压缩后的数据!因此可以简单的认为:只要服务端进行了 Gzip
压缩,文件的体积能瞬间减少一半!
至于如何开启 Gzip
,各个服务平台都有与之相对应的方法。小斑由 Nginx
提供服务,开启 Gzip
模块后,小斑体积骤减至 1.5M
,看起来像个瘦子了!但也仅仅是看起来!我依然是个胖子,只不过神奇的压缩算法把我给变瘦了,滤镜下的胖子,再怎么好看依旧是个胖子!
ps:并不是所有的浏览器都内置了 Gzip
模块,IE
系列就没有(大家赶紧抛弃 IE
吧),因此真实环境下客户端需要一些特殊的请求头,来控制服务器返回的具体内容。
移除冗余代码:锐减 1.4M
冗余代码:就是那些打包到项目中,却没被使用的代码。看到这个标题,有些小伙伴可能会心生不屑,哼!这人连 Webpack
支持 TreeShaking
了都不知道,也不知道是用那个版本的 Webpack
,还在说这些老掉牙的问题,真没意思!
稍等!小斑诞生环境中的 Webpack
早已 4.42.0
,完全支持 TreeShaking
,在这里,小斑只想问一个简单的问题:TreeShaking
能解决样式冗余吗?
不能,直到现在为止,依然没有一种较好的办法,在代码打包时,解决样式冗余的问题。但在代码打包前,也就是编写代码的时候,却可以!
一行代码,700K 的体积!
相信使用过 Antd
组件库的小伙伴,使用 Antd
时第一行代码都长这样:
import "antd/dist/antd.css";
但我悄悄告诉你,这个文件,整整 700K
!请问:有何感想?
小斑为此深入查看了 Antd
代码,了解到其实每个组件下都有单独的样式文件,单独引入样式,就能可以移除多余 CSS
代码。示例如下:
// antd 的公共样式,必须引入
import "antd/lib/style/core/index.less";
// 组件样式,单独引入即可
import "antd/lib/button/style/index.less";
import "antd/lib/xxx/style/index.less";
考虑到,编辑器内,用到了一些高级特性,需要较高版本浏览器的支持,故只考虑了最近几个版本的 Chrome(包括使用 Chromium 核心的其他浏览器)
、FireFox
、Safari
,一些已经成为标准的 CSS3
样式前缀就没必要兼容了,因此小斑的兼容性如下:
"browserslist": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
不要问小斑为什么不兼容 IE
,连它亲爸都不要它了,兼容它干嘛!
一番下来,小斑的样式得到了最大程度的精简(因为 Antd
组件内的样式包含了组件的所有样式,会有部分冗余),样式体积骤减,由原本 900K
减至不到 200K
!经过 Gzip
发生到客户端的体积,不到 100K
!
想高亮吗?收下这 1M 代码!
在小斑诞生之初,便已确定,编辑器必须支持代码高亮,那自然就需要引入 highlight.js
,但这句代码比 Antd
的样式代码更加夸张,好嘛!整整 1M
的 JS
!看到上图中那个大大的 highlight.js
代码块了吗?臃肿不堪,经仔细研究后发现,核心的高亮代码其实很少,绝大部分被 languages
占用!
怎么办?去官网瞧瞧呗,得嘞,不看不知道,一看吓一跳,通过以下形式引入的 highlight.js
包含了整整 189
种的语言解析器!
import hljs from 'highlight.js';
什么?为啥 TreeShaking
没起作用?这和 TreeShaking
的关系可真心不大,高亮是在打包后才用到的模块!那该如何精简?其实官方已经给了答案:按需加载,仅加载需要引入的语言解析器,甚至官方已经为我们推荐好了常用的 38
种语言,一一注入即可!
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
hljs.registerLanguage('javascript', javascript);
虽然整体的代码量由于需要注册 38
种语言增加了一点点,但换来的却是 1M
到 300K
打包体积的变化,值吗?非常的值!
Δ 移除非常用语言后的代码高亮后的整体打包体积。highlight.js
仅占了其中的一小块。
不变的内容,静态化!
有用过小斑编辑器应该知道,小斑支持选择代码的高亮风格以及文章的主题。一开始,小斑包含这部分内容,但后来发现,每次发版,这部分内容永远不变!高亮风格和文章主题不过是一段样式信息而已,为什么不静态化?把这部分内容独立出去!这样不仅小斑可以使用,大家也都可以使用了!
最后,阿飞又折腾出一个新项目:zebra-editor-theme
专注于文章样式的管理,希望大家能多多关注!
当然,开源意味着共享,站点内所有的代码高亮风格,以及文章主题,大家都可以获取,使用,一下是文章主题以及代码风格的样式文件获取方式,当然来目前的文章主题还很少,欢迎大家多多 PR
~
- 代码风格列表:https://zebrastudio.tech/theme/code/index.json 格式:[文件名, 主题短名]
- 文章主题列表:https://zebrastudio.tech/theme/article/index.json 格式:[文件名, 作者, 主题名]
- 代码风格文件:https ://zebrastudio.tech/theme/code/${文件名}
- 文章主题文件:https ://zebrastudio.tech/theme/article/${文件名}
异步:并不是所有的代码,都需要及时加载!
冗余,静态代码已优化完毕,看着这依然有 2.5M
的小斑,阿飞叹了口气:哎,你个胖子啊,看来不能一口气把所有的代码都给你。异步加载模块得用上了!
模块异步加载
异步加载是什么?
一种延迟加载的技术,可以让资源在需要加载的时候,才进行加载。
如何实现?
很简单!一句话的事:
const loadMdAst = () =>
import(
/* webpackChunkName: "mdast", webpackPrefetch: true */ "@textlint/markdown-to-ast"
);
如上,就动态的引入了 Markdown
语法解析器。
如何使用?
import
函数返回 Promise
,该 Promise
会在资源加载结束后进入 resolve
状态,就能获得模块的默认导出了!
const mdAstParse = await loadMdAst();
如上就能在 async
函数中获取 Markdwon
的解析器。但该种方式有个弊端,由于异步函数是有传染性的!会导致一层层的函数都变成异步函数。
React 异步组件
import {lazy, Suspense} from "react";
const Panel = lazy(() =>
import(/* webpackChunkName: "color", webpackPrefetch: true */ "./color-panel"),
);
const ColorBtn = () => {
return (
<Suspense fallback={<Loading size={80} />}>
<Panel />
</Suspense>
)
}
一个简单例子:颜色选择框通过异步组件的形式,加载到项目中,相关内容可翻阅文档:React-Suspense
。
异步,真的值吗?
异步的模块或是组件可以延缓加载时间,但却会导致两个问题:
- 代码被异步污染,任何使用异步加载模块的地方,都会被污染成异步函数;
- 并没有真正的减少请求的体积,但却增加了请求数;
对于第一点,如果说在 async
函数出来之前,大部分小伙伴内心估计是抗拒的,原本流程明确的代码,被套用在一个 Promise
链里,想想都不舒服,不过现在 async/await
函数规范以及制定,异步的获取结果也就一个 await
的事,一点也不麻烦!所以目前,第一点可以忽略不计!
关于第二点,这么说,如果项目中的模块首屏就会被用到,虽然使用了异步的形式,但却依然要等到模块加载完毕才能展示首屏,这类异步的模块其实没有必要。
但如果说项目中有个功能,使用者始终不会用到,不加载应该是最好的选择。还有,如果该功能,不会出现在首屏,但却需要长达 1s
的加载时间,那么它以一种后台加载的形式,默默的在用户使用的时候加载好,对用户来说,也是非常棒的体验了!
因此,对于异步模块,只要满足这两点中的一点,就可以使用了:
- 首屏用不到的模块;
- 需要在一定情况下才显示或使用的模块;
异步模块,虽然不能减少小斑的代码总量,但却能让小斑早一点与你们相见。你说值吗?当然非常的值呀!
缓存:并不是所有的代码,都需要再次加载!
熟悉 Webpack
的小伙伴都应该知道,Webpack
拥有代码分类打包的能力,具体到 Webpack4
其实就是 optimization.splitChunks
这一块大家讨论的很多了,复制粘贴官网的文档也没有意义,就不多说了。在这小斑推荐大家结合官网和网上总结一起看,因为网络上文章的并不一定是最新的,记录下为什么要代码分块(知其所以然比知其然更加重要!):
- 浏览器有自己的缓存策略,分为强缓存和协商缓存,缓存生效的前提在于内容不变;
- 由于项目代码并非一层不变,因此为了避免缓存导致的问题,需要生成版本号;
- 由于版本号的不同,浏览器的缓存策略失效了;
- 但项目的公共代码(比如说
UI
库)其实是可以缓存的; - 如果这些公共代码分包成一个单独的
bundle
不添加版本号,就能被浏览器缓存了! - 但公共代码也会变呀!比如说修复漏洞,增加新功能等!
- 那还是得生成版本号!如果根据打包出来的内容生成版本号,问题迎刃而解!
分包,说到底,是让浏览器缓存生效的一种策略,如果说所有的代码都在一个 bundle
中,为了避免缓存导致的影响,不得已需要添加版本号,但分包之后就不同了,即使版本需要更新,一些包的内容其实没有发生变化,打包出来的文件名也就不会变化,这极大的利用了缓存!
Δ 经异步加载,内容分包后的最终成果
首屏需要加载的内容块如下:
willChange
: 一些可能会发生变化的项目依赖,如antd
,因为可能会使用新的组件;library
: 固定不会的项目依赖,如react
;main
: 小斑的核心代码;
未经 Gzip
前,一共:554K + 344K + 338K ≈ 1M
,Gzip
后仅仅 340K
小斑瘦身大获成功!但是,一切并没有结束!
终极利器 Service Work:0K!
Service Work
可以简单理解为一个介于客户端和服务器之间的一个代理,客户端的请求会通过 Service Work
对外发送,并获取内容,因此 Service Work
具有了控制返回内容的权利,它可以缓存返回的内容,并在第二次请求的时候直接返回数据,这点和缓存很像,但区别就在于,它是可控的,甚至可以提前获取资源,并缓存!
想像一个具体场景,你的网页发版了,用户焦急的等待着浏览器下载新版网页的资源,但一旁使用 Service Work
的网站依旧瞬间打开了老版页面,开始浏览了起来,看起来好像你隔壁的网页没更新。但第二天,你却发现隔壁的网页,不仅更新到了最新版,而且依旧瞬间打开!作为一个积极向上的开发者,我想,你应该理解到了这在用户体验上的差距!
Service Work
可以检查更新,输出原先内容的同时,下载最新内容,并在合适的时候更新缓存内容,对于用户来说,除了第一次访问,之后的每一次都是瞬间打开,完全不需要下载新内容!
这时候有些小伙伴就会问了:这么牛逼的技术,一定很难咯!不!它不仅不难,还很简单,简单到如果你使用脚手架,都不需要考虑这个问题!
所以,你要做的仅仅是打开脚手架里的 Service Work
选项 ~
最后
啰啰嗦嗦写了将近 6000
字,当然,还有一些比如 JS
的代码压缩插件 TerserPlugin
,CSS
的压缩插件 MiniCssExtractPlugin
之类的不提也罢,都是形式化的东西,这些脚手架已经帮开发者弄好了,不需要考虑,当然了解还是需要了解的,那就先把 Webpack
文档看一遍吧 ~
虽然文章仅用了几天写成,但具体到小斑的优化过程却是是一个较长的过程,总结不易,给个 Star
呗。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。