16

console.info

Hello ~ 我是小斑,一个富文本编辑器。今天,咱来聊聊体重,对,没错!就是那令人头疼的体重!任何事物都烦这个体重,当然也包括我,当我的创造者阿飞发布小斑的第一个版本时,小斑足足有 4M 之重!看着控制台茫茫多的流量消耗,他愣住了:你。。。特么的这么怎么胖,足足 4M(包括 CSS)!但小斑我也很无奈呀!代码是你写的,怪我咯!

软件发展总归避免不了两个过程:野蛮生长 & 精心优化,小斑长算是长出来了,但这一身肥膘得好好减减!

image

哝!上图,就是我刚诞生时,所包含的 JS 模块,足足 3M 之多!但小斑的核心代码仅仅存在于右下角蓝色的方块内,其他都属于增强体验的代码,包括 UI 框架,代码高亮,表情的便捷输入等等。

接下来,小斑瘦身记,正式开始。


数据压缩:从 4M 到 1.5M

提到数据压缩,就不得不提一个鼎鼎大名的数据压缩算法:Gzip

Gzip 全名: GNU zip 开源的数据压缩算法,广泛应用于网络传输。大家可能会有疑惑,数据压缩算法这么多,凭啥 Gzip 如此出名?因为压缩比高?

no!no!no!仅仅因为它是 GNU 开头,互联网诞生于一个专利横飞的年代,各种好用的压缩算法受限于专利,并不能使用在开放的互联网上,因此大家需要一个高效且开源的压缩算法,Gzip 因此诞生!

使用 Gzip 很简单,过程大概也就 3 步:

  1. 服务端使用 Gzip 将需要发送的数据进行压缩;
  2. 客户端将接收的数据并进行 Gzip 解压;
  3. 客户端使用解压后的数据;

因此,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 核心的其他浏览器)FireFoxSafari ,一些已经成为标准的 CSS3 样式前缀就没必要兼容了,因此小斑的兼容性如下:

"browserslist": [
     "last 1 chrome version",
     "last 1 firefox version",
     "last 1 safari version"
 ]

不要问小斑为什么不兼容 IE,连它亲爸都不要它了,兼容它干嘛!

一番下来,小斑的样式得到了最大程度的精简(因为 Antd 组件内的样式包含了组件的所有样式,会有部分冗余),样式体积骤减,由原本 900K 减至不到 200K!经过 Gzip 发生到客户端的体积,不到 100K

想高亮吗?收下这 1M 代码!

在小斑诞生之初,便已确定,编辑器必须支持代码高亮,那自然就需要引入 highlight.js,但这句代码比 Antd 的样式代码更加夸张,好嘛!整整 1MJS!看到上图中那个大大的 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 种语言增加了一点点,但换来的却是 1M300K 打包体积的变化,值吗?非常的值!

image

Δ 移除非常用语言后的代码高亮后的整体打包体积。highlight.js 仅占了其中的一小块。


不变的内容,静态化!

有用过小斑编辑器应该知道,小斑支持选择代码的高亮风格以及文章的主题。一开始,小斑包含这部分内容,但后来发现,每次发版,这部分内容永远不变!高亮风格和文章主题不过是一段样式信息而已,为什么不静态化?把这部分内容独立出去!这样不仅小斑可以使用,大家也都可以使用了!

最后,阿飞又折腾出一个新项目:zebra-editor-theme 专注于文章样式的管理,希望大家能多多关注!

当然,开源意味着共享,站点内所有的代码高亮风格,以及文章主题,大家都可以获取,使用,一下是文章主题以及代码风格的样式文件获取方式,当然来目前的文章主题还很少,欢迎大家多多 PR ~


异步:并不是所有的代码,都需要及时加载!

冗余,静态代码已优化完毕,看着这依然有 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 的加载时间,那么它以一种后台加载的形式,默默的在用户使用的时候加载好,对用户来说,也是非常棒的体验了!

因此,对于异步模块,只要满足这两点中的一点,就可以使用了:

  1. 首屏用不到的模块;
  2. 需要在一定情况下才显示或使用的模块;

异步模块,虽然不能减少小斑的代码总量,但却能让小斑早一点与你们相见。你说值吗?当然非常的值呀!


缓存:并不是所有的代码,都需要再次加载!

熟悉 Webpack 的小伙伴都应该知道,Webpack 拥有代码分类打包的能力,具体到 Webpack4 其实就是 optimization.splitChunks 这一块大家讨论的很多了,复制粘贴官网的文档也没有意义,就不多说了。在这小斑推荐大家结合官网和网上总结一起看,因为网络上文章的并不一定是最新的,记录下为什么要代码分块(知其所以然比知其然更加重要!):

  • 浏览器有自己的缓存策略,分为强缓存和协商缓存,缓存生效的前提在于内容不变;
  • 由于项目代码并非一层不变,因此为了避免缓存导致的问题,需要生成版本号;
  • 由于版本号的不同,浏览器的缓存策略失效了;
  • 但项目的公共代码(比如说 UI 库)其实是可以缓存的;
  • 如果这些公共代码分包成一个单独的 bundle 不添加版本号,就能被浏览器缓存了!
  • 但公共代码也会变呀!比如说修复漏洞,增加新功能等!
  • 那还是得生成版本号!如果根据打包出来的内容生成版本号,问题迎刃而解!

分包,说到底,是让浏览器缓存生效的一种策略,如果说所有的代码都在一个 bundle 中,为了避免缓存导致的影响,不得已需要添加版本号,但分包之后就不同了,即使版本需要更新,一些包的内容其实没有发生变化,打包出来的文件名也就不会变化,这极大的利用了缓存!

image

Δ 经异步加载,内容分包后的最终成果

首屏需要加载的内容块如下:

  • willChange: 一些可能会发生变化的项目依赖,如 antd,因为可能会使用新的组件;
  • library: 固定不会的项目依赖,如 react
  • main: 小斑的核心代码;

未经 Gzip 前,一共:554K + 344K + 338K ≈ 1MGzip 后仅仅 340K 小斑瘦身大获成功!但是,一切并没有结束!


终极利器 Service Work:0K!

Service Work 可以简单理解为一个介于客户端和服务器之间的一个代理,客户端的请求会通过 Service Work 对外发送,并获取内容,因此 Service Work 具有了控制返回内容的权利,它可以缓存返回的内容,并在第二次请求的时候直接返回数据,这点和缓存很像,但区别就在于,它是可控的,甚至可以提前获取资源,并缓存!

想像一个具体场景,你的网页发版了,用户焦急的等待着浏览器下载新版网页的资源,但一旁使用 Service Work 的网站依旧瞬间打开了老版页面,开始浏览了起来,看起来好像你隔壁的网页没更新。但第二天,你却发现隔壁的网页,不仅更新到了最新版,而且依旧瞬间打开!作为一个积极向上的开发者,我想,你应该理解到了这在用户体验上的差距!

Service Work 可以检查更新,输出原先内容的同时,下载最新内容,并在合适的时候更新缓存内容,对于用户来说,除了第一次访问,之后的每一次都是瞬间打开,完全不需要下载新内容!

这时候有些小伙伴就会问了:这么牛逼的技术,一定很难咯!不!它不仅不难,还很简单,简单到如果你使用脚手架,都不需要考虑这个问题!

所以,你要做的仅仅是打开脚手架里的 Service Work 选项 ~

最后

啰啰嗦嗦写了将近 6000 字,当然,还有一些比如 JS 的代码压缩插件 TerserPluginCSS 的压缩插件 MiniCssExtractPlugin 之类的不提也罢,都是形式化的东西,这些脚手架已经帮开发者弄好了,不需要考虑,当然了解还是需要了解的,那就先把 Webpack 文档看一遍吧 ~

虽然文章仅用了几天写成,但具体到小斑的优化过程却是是一个较长的过程,总结不易,给个 Star 呗。

本篇文章由斑马编辑器编辑并生成,我是小斑,我为自己带盐 ~


aco
1k 声望139 粉丝

出卖技术,买糖吃。