-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
前端工程与性能优化 #3
Comments
好文。网页确实是需要编译出来的,这是前端工程化的必然结果。 |
niubility |
👍 |
写得好详细。。 |
受教了 |
to 作者: 这些经验很值得介绍出去的呢,为什么没有类似的框架是专门做这类的工作的呢?作者有否考虑这样的框架? |
可了解下fis
|
资源合并那里补充一个详细的方案 静态资源自动合并系统~~ |
一看就知道是我想要的!希望看到PHP版本的demo哟 |
niubi |
拜读了您的文章,有个小疑问,每个文件改动后生产md5后缀,多次上线后线上会产生: ··· 也许是我要洁癖,但是这样循环N次后,上线的全量包会越来约大,不知道你们是如何处理这个的。 |
每次上线,只有修改过的文件才会出现新的md5戳,所以文件冗余没有想象中的那么多,我们做过测试,比较频繁修改的业务模块大概每年会产生100m左右的冗余,我们预计每3年有必要清理一次。 清理的时候,写一个脚本,根据文件名规则找到最后访问的文件然后删除其他的。活着干脆某次上线把发布后的文件之外的其他文件都清理一次,总之这个不成问题。 洁癖问题也没什么,因为文件源码是干干净净的,上线前做一次构建,把东西直接扔到服务器,服务器或许文件会比较多,但没有恶心到工程师。 |
学生一枚,看完之后启发太大了,对我来说打开了一个新的世界之门 |
好文 |
棒棒棒 |
看是看明白了,但是比如:HTML是后端们JAVA写的动态页面,前端们只写JS,css,然后静态资源发布后,生成了新的md5,那么JAVA写的页面里怎么去获取这个新的MD5,以保证加载正确的静态资源。是要在前端静态文件服务器上搞个监控,把新的MD5存某个地方,然后JAVA那边每次请求页面都要获取下新的MD5,替换生成新的链接? |
java写动态页面不是?不要让他们在java的模板中写这样的代码: <script src="a.js"></script> 改成写这样的代码: <fis:require id="a.js"/> 这个 {
"a.js" : {
"url": "/static/js/a_0fa0c3b.js",
"deps": [ "b.js" ]
},
"b.js" : {
"url": "/static/js/b_4cb04f9.js"
}
} 然后,我们把这个资源表和java的动态页面放在一起。前面提到的模板中的那个 最后,在java动态页面生成html之前,把收集到的两个js标签用字符串替换的方式生成script标签插入到页面上,得到: <script src="/static/js/a_0fa0c3b.js"></script>
<script src="/static/js/b_4cb04f9.js"></script> 我有一个项目展示了这个思路的整个实现过程: https://github.com/fouber/fis-java-jsp |
这个
|
@fouber 嗯,谢谢,有点明白了。还有几点疑问: |
fis 只生成 资源表 |
@nimojs |
每个框架自己实现,算法描述在文章中已经给出了。 |
不同的公司、不同的业务可能又会不同的定制,鉴于资源管理框架代码非常少,又通用性非常低,所以fis只给出使用描述和示例,并不给出固定的框架。 比如我在松鼠公司,以移动端为主,设计的资源管理框架又会有一定的改造,前端架构的设计精髓也就体现在这么几行代码上了 |
@fouber |
扫描所有文件,计算文件的摘要,然后生成url。再以文件工程路径为key,建立map表,整个过程不会替换任何文件内容,只是建立表。
都是线下编译。线下设置好js、css要发布的server1的域名、路径,然后release,生成编译后的代码和map,把代码发布到server1上,把map发布到server2上,map中写入的js、css的路径都是符合预期的。构建工具扫描的并不是简单的编译后的结果。我们用工具读取所有文件,然后逐个编译,然后把编译后的结果发布为带md5戳的资源,同时在map中记录的是
其他语言可以根据fis的map.json结构,和fis资源管理的思想自己实现这个框架,并不复杂 |
文章写的不错.有很多借鉴的地方 |
好文章 |
6 |
现在是2023年了,想请教一下,文中所述的这些工程化的经验还适用于当前的前端构建吗? |
您的来信我已收到~我会在看到的第一时间回复给您~
|
你好,如果你收到这封邮件。可能 只是因为我想测试一下假期自动回复功能。。
|
报名表已经收到咯!请继续学习前端方向的基础知识,后期我们可能会组织培训噢!
|
谢谢。您的邮件已收到,我会尽快处理。Thank you. Your email is received and will be handled as soon as possible.
|
邮件已收到 我会尽快给您回复 thks
|
您好,邮件已经收到,我将尽快给您回复。谢谢~~
|
我收到了
|
这是来自QQ邮箱的假期自动回复邮件。
您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
邮件已收到~
|
这是来自QQ邮箱的假期自动回复邮件。
您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
这是来自QQ邮箱的假期自动回复邮件
|
Hello,我是林昕,我已收到来信~
|
这是来自QQ邮箱的假期自动回复邮件。
您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
好的,邮件我已收到!我会尽快阅读并回复的!
|
收到。谢谢
|
每个参与过开发企业级web应用的前端工程师或许都曾思考过前端性能优化方面的问题。我们有雅虎14条性能优化原则,还有两本很经典的性能优化指导书:《高性能网站建设指南》、《高性能网站建设进阶指南》。经验丰富的工程师对于前端性能优化方法耳濡目染,基本都能一一列举出来。这些性能优化原则大概是在7年前提出的,对于web性能优化至今都有非常重要的指导意义。
然而,对于构建大型web应用的团队来说,要坚持贯彻这些优化原则并不是一件十分容易的事。因为优化原则中很多要求是与工程管理相违背的,比如
把css放在头部
和把js放在尾部
这两条原则,我们不能让团队的工程师在写样式和脚本引用的时候都去修改一个相同的页面文件。这样做会严重影响团队成员间并行开发的效率,尤其是在团队有版本管理的情况下,每天要花大量的时间进行代码修改合并,这项成本是难以接受的。因此在前端工程界,总会看到周期性的性能优化工作,辛勤的前端工程师们每到月圆之夜就会倾巢出动根据优化原则做一次性能优化。本文将从一个全新的视角来思考web性能优化与前端工程之间的关系,揭示前端性能优化在前端架构及开发工具设计层面的实现思路。
性能优化原则及分类
po主先假设本文的读者是有前端开发经验的工程师,并对企业级web应用开发及性能优化有一定的思考,因此我不会重复介绍雅虎14条性能优化原则。如果您没有这些前续知识,请移步 这里 来学习。
首先,我们把雅虎14条优化原则,《高性能网站建设指南》以及《高性能网站建设进阶指南》中提到的优化点做一次梳理,按照优化方向分类,可以得到这样一张表格:
减少DNS查找,配置ETag,使AjaX可缓存
目前大多数前端团队可以利用 yui compressor 或者 google closure compiler 等压缩工具很容易做到
精简Javascript
这条原则;同样的,也可以使用图片压缩工具对图像进行压缩,实现图像优化
原则。这两条原则是对单个资源的处理,因此不会引起任何工程方面的问题。很多团队也通过引入代码校验流程来确保实现避免css表达式
和避免重定向
原则。目前绝大多数互联网公司也已经开启了服务端的Gzip压缩,并使用CDN实现静态资源的缓存和快速访问;一些技术实力雄厚的前端团队甚至研发出了自动CSS Sprites工具,解决了CSS Sprites在工程维护方面的难题。使用“查找-替换”思路,我们似乎也可以很好的实现划分主域
原则。我们把以上这些已经成熟应用到实际生产中的优化手段去除掉,留下那些还没有很好实现的优化原则。再来回顾一下之前的性能优化分类:
有很多顶尖的前端团队可以将上述还剩下的优化原则也都一一解决,但业界大多数团队都还没能很好的解决这些问题。因此,本文将就这些原则的解决方案做进一步的分析与讲解,从而为那些还没有进入前端工业化开发的团队提供一些基础技术建设意见,也借此机会与业界顶尖的前端团队在工业化工程化方向上交流一下彼此的心得。
静态资源版本更新与缓存
缓存利用
分类中保留了添加Expires头
和配置ETag
两项。或许有些人会质疑,明明这两项只要配置了服务器的相关选项就可以实现,为什么说它们难以解决呢?确实,开启这两项很容易,但开启了缓存后,我们的项目就开始面临另一个挑战:如何更新这些缓存?
相信大多数团队也找到了类似的答案,它和《高性能网站建设指南》关于“添加Expires头”所说的原则一样——修订文件名。即:
思路没错,但要怎么改变链接呢?变成什么样的链接才能有效更新缓存,又能最大限度避免那些没有修改过的文件缓存不失效呢?
先来看看现在一般前端团队的做法:
接下来,项目升级,比如页面上的html结构发生变化,对应还要修改
a.js
这个文件,得到的构建结果如下:为了触发用户浏览器的缓存更新,我们需要更改静态资源的url地址,如果采用构建信息(时间戳、版本号等)作为url修改的依据,如上述代码所示,我们只修改了一个a.js文件,但再次构建会让所有请求都更改了url地址,用户再度访问页面那些没有修改过的静态资源的(b.js,b.js,c.js,d.js,e.js)的浏览器缓存也一同失效了。
此外,采用添加query的方式来清除缓存还有一个弊端,就是
覆盖式发布
的上线问题。采用query更新缓存的方式实际上要覆盖线上文件的,index.html和a.js总有一个先后的顺序,从而中间出现一段或大或小的时间间隔。尤其是当页面是后端渲染的模板的时候,静态资源和模板是部署在不同的机器集群上的,上线的过程中,静态资源和页面文件的部署时间间隔可能会非常长,对于一个大型互联网应用来说即使在一个很小的时间间隔内,都有可能出现新用户访问。在这个时间间隔中,访问了网站的用户会发生什么情况呢?
这就是为什么大型web应用在版本上线的过程中经常会较集中的出现前端报错日志的原因,也是一些互联网公司选择加班到半夜等待访问低峰期再上线的原因之一。
对于静态资源缓存更新的问题,目前来说最优方案就是
基于文件内容的hash版本冗余机制
了。也就是说,我们希望项目源码是这么写的:发布后代码变成
也就是a.js发布出来后被修改了文件名,产生一个新文件,并不是覆盖已有文件。其中”_82244e91”这串字符是根据a.js的文件内容进行hash运算得到的,只有文件内容发生变化了才会有更改。由于将文件发布为带有hash的新文件,而不是同名文件覆盖,因此不会出现上述说的那些问题。同时,这么做还有其他的好处:
虽然这种方案是相比之下最完美的解决方案,但它无法通过手工的形式来维护,因为要依靠手工的形式来计算和替换hash值,并生成相应的文件,将是一项非常繁琐且容易出错的工作,因此我们需要借助工具来处理。
用grunt来实现md5功能是非常困难的,因为grunt只是一个task管理器,而md5计算需要构建工具具有递归编译的能,而不是简单的任务调度。考虑这样的例子:
由于我们的资源版本号是通过对文件内容进行hash运算得到,如上图所示,index.html中引用的a.css文件的内容其实也包含了a.png的hash运算结果,因此我们在修改index.html中a.css的引用时,不能直接计算a.css的内容hash,而是要先计算出a.png的内容hash,替换a.css中的引用,得到了a.css的最终内容,再做hash运算,最后替换index.html中的引用。
grunt等task-based的工具是很难在task之间协作处理这样的需求的。
在解决了基于内容hash的版本更新问题之后,我们可以将所有前端静态资源开启永久强缓存,每次版本发布都可以首先让静态资源全量上线,再进一步上线模板或者页面文件,再也不用担心各种缓存和时间间隙的问题了!
静态资源管理与模块化框架
解决了静态资源缓存问题之后,让我们再来看看前面的优化原则表还剩些什么:
很不幸,剩下的优化原则都不是使用工具就能很好实现的。或许有人会辩驳:“我用某某工具可以实现脚本和样式表合并”。嗯,必须承认,使用工具进行资源合并并替换引用或许是一个不错的办法,但在大型web应用,这种方式有一些非常严重的缺陷,来看一个很熟悉的例子 :
某个web产品页面有A、B、C三个资源
工程师根据“减少HTTP请求”的优化原则合并了资源
产品经理要求C模块按需出现,此时C资源已出现多余的可能
C模块不再需要了,注释掉吧!代码1秒钟搞定,但C资源通常不敢轻易剔除
不知不觉中,性能优化变成了性能恶化……
事实上,使用工具在线下进行静态资源合并是无法解决资源按需加载的问题的。如果解决不了按需加载,则必会导致资源的冗余;此外,线下通过工具实现的资源合并通常会使得资源加载和使用的分离,比如在页面头部或配置文件中写资源引用及合并信息,而用到这些资源的html组件写在了页面其他地方,这种书写方式在工程上非常容易引起维护不同步的问题,导致使用资源的代码删除了,引用资源的代码却还在的情况。因此,在工业上要实现资源合并至少要满足如下需求:
将以上要求综合考虑,不难发现,单纯依靠前端技术或者工具处理是很难达到这些理想要求的。
接下来我会讲述一种新的模板架构设计,用以实现前面说到那些性能优化原则,同时满足工程开发和维护的需要,这种架构设计的核心思想就是:
考虑一段这样的页面代码:
根据资源合并需求中的第二项,我们希望资源引用与使用能尽量靠近,这样将来维护起来会更容易一些,因此,理想的源码是:
当然,把这样的页面直接送达给浏览器用户是会有严重的页面闪烁问题的,所以我们实际上仍然希望最终页面输出的结果还是如最开始的截图一样,将css放在头部输出。这就意味着,页面结构需要有一些调整,并且有能力收集资源加载需求,那么我们考虑一下这样的源码(以php为例):
在页面的头部插入一个html注释
<!--[CSS LINKS PLACEHOLDER]-->
作为占位,而将原来字面书写的资源引用改成模板接口require_static
调用,该接口负责收集页面所需资源。require_static接口实现非常简单,就是准备一个数组,收集资源引用,并且可以去重。最后在页面输出的前一刻,我们将require_static在运行时收集到的
a.css
、b.css
、c.css
三个资源拼接成html标签,替换掉注释占位<!--[CSS LINKS PLACEHOLDER]-->
,从而得到我们需要的页面结构。经过实践总结,可以发现模板层面只要实现三个开发接口,就可以比较完美的实现目前遗留的大部分性能优化原则,这三个接口分别是:
将js放在页面底部
原则。实现了这些接口之后,一个重构后的模板页面的源代码可能看起来就是这样的了:
而最终在模板解析的过程中,资源收集与去重、页面script收集、占位符替换操作,最终从服务端发送出来的html代码为:
不难看出,我们目前已经实现了
按需加载
,将脚本放在底部
,将样式表放在头部
三项优化原则。前面讲到静态资源在上线后需要添加hash戳作为版本标识,那么这种使用模板语言来收集的静态资源该如何实现这项功能呢?
考虑这样的目录结构:
如果我们可以使用工具扫描整个project目录,然后创建一张资源表,同时记录每个资源的部署路径,得到这样的一张表:
基于这张表,我们就很容易实现
require_static(file_id)
,load_widget(widget_id)
这两个模板接口了。以load_widget为例:利用查表来解决md5戳的问题,这样,我们的页面最终送达给用户的结果就是这样的:
接下来,我们讨论基于表的设计思想上是如何实现静态资源合并的。或许有些团队使用过combo服务,也就是我们在最终拼接生成页面资源引用的时候,并不是生成多个独立的link标签,而是将资源地址拼接成一个url路径,请求一种线上的动态资源合并服务,从而实现减少HTTP请求的需求,比如前面的例子,稍作调整即可得到这样的结果:
这个
/??file1,file2,file3,…
的url请求响应就是动态combo服务提供的,它的原理很简单,就是根据url找到对应的多个文件,合并成一个文件来响应请求,并将其缓存,以加快访问速度。这种方法很巧妙,有些服务器甚至直接集成了这类模块来方便的开启此项服务,这种做法也是大多数大型web应用的资源合并做法。但它也存在一些缺陷:
对于上述第二条缺陷,可以举个例子来看说明:
a,b,c,d
a,b,e,f
很明显,如果combo服务能聪明的知道A页面使用的资源引用为 /??
a,b
和 /??c,d
,而B页面使用的资源引用为 /??a,b
和 /??e,f
就好了。这样当用户在访问A页面之后再访问B页面时,只需要下载B页面的第二个combo文件即可,第一个文件已经在访问A页面时缓存好了的。基于这样的思考,我们在资源表上新增了一个字段,取名为
pkg
,就是资源合并生成的新资源,表的结构会变成:相比之前的表,可以看到新表中多了一个pkg字段,并且记录了打包后的文件所包含的独立资源。这样,我们重新设计一下 require_static、load_widget 这两个模板接口,实现这样的逻辑:
比如执行
require_static('bootstrap.js')
,查表得知bootstrap.js被打包在了p1
中,因此取出p1包的url/pkg/lib_cef213d.js
,并且记录页面已加载了jquery.js
和bootstrap.js
两个资源。这样一来,之前的模板代码执行之后得到的html就变成了:虽然这种策略请求有4个,不如combo形式的请求少,但可能在统计上是性能更好的方案。由于两个lib打包的文件修改的可能性很小,因此这两个请求的缓存利用率会非常高,每次项目发布后,用户需要重新下载的静态资源可能要比combo请求节省很多带宽。
此时,我们又引入了一个新的问题:如何决定哪些文件被打包?
从经验来看,项目初期可以采用人工配置的方式来指定打包情况,比如:
但随着系统规模的增大,人工配置会带来非常高的维护成本,此时需要一个辅助系统,通过分析线上访问日志和静态资源组合加载情况来自动生成这份配置文件,系统设计如图:
至此,我们通过基于表的静态资源管理系统和三个模板接口实现了几个重要的性能优化原则,现在我们再来回顾一下前面的性能优化原则分类表,剔除掉已经做到了的,看看还剩下哪些没做到的:
拆分初始化负载
的目标是将页面一开始加载时不需要执行的资源从所有资源中分离出来,等到需要的时候再加载。工程师通常没有耐心去区分资源的分类情况,但我们可以利用组件化框架接口来帮助工程师管理资源的使用。还是从例子开始思考,如果我们有一个js文件是用户交互后才需要加载的,会怎样呢:很明显,
dialog.js
这个文件我们不需要在初始化的时候就加载,因此它应该在后续的交互中再加载,但文件都加了md5戳,我们如何能在浏览器环境中知道加载的url呢?我就不多解释代码的执行过程了,大家看到完整的html输出就能理解是怎么回事了:
dialog.js不会在页面以script src的形式输出,而是变成了资源注册,这样,当页面点击触发require.async执行的时候,async函数才会查表找到资源的url并加载它,加载完毕后触发回调函数。
到目前为止,我们又以架构的形式实现了一项优化原则(拆分初始化负载),回顾我们的优化分类表,现在仅有两项没能做到了:
剩下的两项优化原则要做到并不容易,真正可缓存的Ajax在现实开发中比较少见,而
尽早刷新文档的输出
原则facebook在2010年的velocity上 提到过,就是BigPipe技术。当时facebook团队还讲到了Quickling和PageCache两项技术,其中的PageCache算是比较彻底的实现Ajax可缓存的优化原则了。由于篇幅关系,就不在此展开了,后续还会撰文详细解读这两项技术。总结
其实在前端开发工程管理领域还有很多细节值得探索和挖掘,提升前端团队生产力水平并不是一句空话,它需要我们能对前端开发及代码运行有更深刻的认识,对性能优化原则有更细致的分析与研究。在前端工业化开发的所有环节均有可节省的人力成本,这些成本非常可观,相信现在很多大型互联网公司也都有了这样的共识。
本文只是将这个领域中很小的一部分知识的展开讨论,抛砖引玉,希望能为业界相关领域的工作者提供一些不一样的思路。
The text was updated successfully, but these errors were encountered: