前端工程与性能优化

上传人:桂梅 文档编号:173880996 上传时间:2022-12-13 格式:DOCX 页数:30 大小:212.88KB
返回 下载 相关 举报
前端工程与性能优化_第1页
第1页 / 共30页
前端工程与性能优化_第2页
第2页 / 共30页
前端工程与性能优化_第3页
第3页 / 共30页
点击查看更多>>
资源描述
前端工程与性能优化每个参与过开发企业级 web 应用的前端工程师或许都曾思考过前端性能优化方面的 问题。我们有雅虎 14 条性能优化原则,还有两本很经典的性能优化指导书:高性能网 站建设指南、高性能网站建设进阶指南。经验丰富的工程师对于前端性能优化方法耳 濡目染,基本都能一一列举出来。这些性能优化原则大概是在 7 年前提出的,对于 web 性 能优化至今都有非常重要的指导意义。然而,对于构建大型 web 应用的团队来说,要坚持贯彻这些优化原则并不是一件十分 容易的事。因为优化原则中很多要求与工程管理相违背,比如“把 css 放在头部”和“把 js 放在尾部”这两条原则,我们不能让整个团队的工程师在写样式和脚本引用的时候都去修改 同一份的页面文件。这会严重影响团队成员间并行开发的效率,尤其是在团队有版本管理的 情况下,每天要花大量的时间进行代码修改合并,这项成本是难以接受的。因此在前端工程 界,总会看到周期性的性能优化工作,辛勤的前端工程师们每到月圆之夜就会倾巢出动根据 优化原则做一次最佳实践。本文从一个全新的视角来思考 web 性能优化与前端工程之间的关系,通过解读百度前 端集成解决方案小组(F.I.S)在打造高性能前端架构并统一百度40多条前端产品线的过 程中所经历的技术尝试,揭示前端性能优化在前端架构及开发工具设计层面的实现思路。性能优化原则及分类 笔者先假设本文的读者是有前端开发经验的工程师,并对企业级 web 应用开发及性能 优化有一定的思考。因此我不会重复介绍雅虎14条性能优化原则,如果您没有这些前续 知识的,请移步这里来学习。首先,我们把雅虎 14 条优化原则,高性能网站建设指南以及高性能网站建设进阶指南中提到的优化点做一次梳理,如果按照优化方向分类可以得到这样一张表格:优化方 向优化手段请求 数量合并脚本和样式表,CSS Sprites,拆分初始化负载,划分主域请求 带宽开启GZip,精简JavaScrip t,移除重复脚本,图像优化缓存使用CDN,使用外部JavaScript和CSS,添加Expires头,减少DNS优化方 向优化手段利用查找,配置ETag,使AjaX可缓存页面 结构将样式表放在顶部,将脚本放在底部,尽早刷新文档的输出代码 校验避免CSS表达式,避免重定向目前大多数前端团队可以利用yui compressor或者google closure compiler等压缩工具很容易做到“精简 javascript ”这条原则,同样的,也可以使用图片压缩工具对图像进行压缩,实现“图像优化”原则,这两条原则是对单个资源的处理,因此不会引起任何工程 方面的问题;很多团队也通过引入代码校验流程来确保实现“避免 css 表达式”和“避免重定 向”原则;目前绝大多数互联网公司也已经开启了服务端的 Gzip 压缩,并使用 CDN 实现 静态资源的缓存和快速访问;一些技术实力雄厚的前端团队甚至研发出了自动 CSSSprites工具,解决了 CSS Sprites在工程维护方面的难题。使用查找-替换思路, 我们似乎也可以很好的实现“划分主域”原则。我们把以上这些已经成熟应用到实际生产中的优化手段去除掉,留下那些还没有很好实 现的优化原则,再来回顾一下之前的性能优化分类:优化方向优化手段请求数量合并脚本和样式表,拆分初始化负载请求带宽移除重复脚本缓存利用添加Expires头,配置ETag,使Ajax可缓存页面结构将样式表放在顶部,将脚本放在底部,尽早刷新文档的输出诚然,不可否认现在有很多顶尖的前端团队可以将上述还剩下的优化原则也都一一解决 但业界大多数团队都还没能很好的解决这些问题,因此接下来本文将就这些原则的解决方案 做进一步的分析与讲解,从而为那些还没有进入前端工业化开发的团队提供一些基础技术建 设意见,也借此机会与业界顶尖的前端团队在工业化工程化方向上交流一下彼此的心得。静态资源版本更新与缓存如表格 2 所示,在缓存利用”分类中保留了添加 Expires 头”和配置 ETag ”两项, 或许有些人会质疑,明明这两项只要配置了服务器的相关选项就可以实现,为什么说它们难以解决呢?确实,开启这两项很容易,但开启了缓存后,我们的项目就开始面临另一个挑战: 如何更新这些缓存。相信大多数团队也找到了类似的答案,它和高性能网站建设指南关于“添加 Expires 头”所说的原则一样修订文件名。即:思路没错,但要怎么改变链接呢?变成什么样的链接才能有效更新缓存,又能最大限度 避免那些没有修改过的文件缓存不失效呢?先来看看现在一般前端团队的做法:script type=text/javascript src=a.js?t=20130825/script或者script type=text/javascript src=a.js?v=1.0.0/script大家会采用添加 query 的形式修改链接。这样做是比较直观的解决方案,但在访问量 较大的网站,这么做可能将面临一些新的问题。通常一个大型的 web 应用几乎每天都会有迭代和更新,发布新版本也就是发布新的静 态资源和页面的过程。以上述代码为例,假设现在线上运行着 index.html 文件,并且使 用了线上的a.js资源。index.html的内容为:script type=text/javascript src=a.js?v=1.0.0/script这次我们更新了页面中的一些内容,得到一个 index.html 文件,并开发了新的与之匹配的a.js资源来完成页面交互,新的index.html文件的内容因此而变成了:script type=text/javascript src=a.js?v=1.0.1/script好了,现在要开始将两份新的文件发布到线上去。可以看到, a.html 和 a.js 的资源 实际上是要覆盖线上的同名文件的。不管怎样,在发布的过程中, index.html 和 a.js 总 有一个先后的顺序,从而中间出现一段或大或小的时间间隔。对于一个大型互联网应用来说 即使在一个很小的时间间隔内,都有可能出现新用户访问,而在这个时间间隔中访问了网站 的用户会发生什么情况呢:1. 如果先覆盖index.html,后覆盖a.js,用户在这个时间间隙访问,会得到新的 index.html 配合旧的 a.js 的情况,从而出现错误的页面。2. 如果先覆盖a.js,后覆盖index.html,用户在这个间隙访问,会得到旧的 index.html 配合新的 a.js 的情况,从而也出现了错误的页面。这就是为什么大型 web 应用在版本上线的过程中经常会较集中的出现前端报错日志 的原因,也是一些互联网公司选择加班到半夜等待访问低峰期再上线的原因之一。此外,由 于静态资源文件版本更新是“覆盖式”的,而页面需要通过修改 query 来更新,对于使用CDN 缓存的 web 产品来说,还可能面临 CDN 缓存攻击的问题。我们再来观察一下前面说的版本更新手段:script type=text/javascript src=a.js?v=1.0.0/script我们不难预测,a.js的下一个版本是“ 1.0.1 ,那么就可以刻意构造一串这样的请求“ a.js?v=1.0.1 ”、“ a.js?v=1.0.2 ”、让 CDN 将当前的资源缓存为“未来的版本”。 这样当这个页面所用的资源有更新时,即使更改了链接地址,也会因为 CDN 的原因返回 给用户旧版本的静态资源,从而造成页面错误。即便不是刻意制造的攻击,在上线间隙出现 访问也可能导致区域性的 CDN 缓存错误。此外,当版本有更新时,修改所有引用链接也是一件与工程管理相悖的事,至少我们需 要一个可以“查找 - 替换”的工具来自动化的解决版本号修改的问题。对付这个问题,目前来说最优方案就是 基于文件内容的 hash 版本冗余机制 了。也就是说,我们希望工程师源码是这么写的:script type=text/javascript src=a.js/script但是线上代码是这样的:script type=text/javascript src=a_8244e91js第二天vhtril of Bhtttl of chref-B-C.cssS工程师根据“减少 HTTP 请求”的优化原则合并了资源第三天if fu5er_has_C htirl of 匚(/!*clink hrefA- htl of A htnl of B产品经理要求 C 模块按需出现,此时 C 资源已出现多余的可能C 模块不再需要了,注释掉吧!但 C 资源通常不敢轻易剔除肩來eUe htnilHvhtiri/IOof G_href/R.日y,html不知不觉中,性能优化变成了性能恶化事实上,使用工具在线下进行静态资源合并是无法解决资源按需加载的问题的。如果解 决不了按需加载,则势必会导致资源的冗余;此外,线下通过工具实现的资源合并通常会使 得资源加载和使用的分离,比如在页面头部或配置文件中写资源引用及合并信息,而用到这 些资源的 html 组件写在了页面其他地方,这种书写方式在工程上非常容易引起维护不同 步的问题,导致使用资源的代码删除了,引用资源的代码却还在的情况。因此,在工业上要 实现资源合并至少要满足如下需求:1. 确实能减少 HTTP 请求,这是基本要求(合并)2. 在使用资源的地方引用资源(就近依赖),不使用不加载(按需)3. 虽然资源引用不是集中书写的,但资源引用的代码最终还能出现在页面头部(css) 或尾部(js)4. 能够避免重复加载资源(去重)将以上要求综合考虑,不难发现,单纯依靠前端技术或者工具处理的是很难达到这些理 想要求的。现代大型 web 应用所展示的页面绝大多数都是使用服务端动态语言拼接生成的 有的产品使用模板引擎,比如smarty、velocity,有的则干脆直接使用动态语言,比如php、python。无论使用哪种方式实现,前端工程师开发的html绝大多数最终都不是以静态的html 在线上运行的,接下来我会讲述一种新的模板架构设计,用以实现前面说到那些性能优化原则,同时满足工程开发和维护的需要,这种架构设计的核心思想就是:考虑一段这样的页面代码:hello worldhtml of Ahtml of Bhtml of C根据资源合并需求中的第二项,我们希望资源引用与使用能尽量靠近,这样将来维护起 来会更容易一些,因此,理想的源码是:hello worldlinkrel=stylesheettype=text/csshref=A.cssdivhtmlofAlinkrel=stylesheettype=text/csshref=B.cssdivhtmlofBlinkrel=stylesheettype=text/csshref=C.cssdivhtmlofC/div当然,把这样的页面直接送达给浏览器用户是会有严重的页面闪烁问题的,所以我们实际上仍然希望最终页面输出的结果还是如最开始的截图一样,将css放在头部输出。这就意味着,页面结构需要有一些调整,并且有能力收集资源加载需求,那么我们考虑一下这样 的源码:hello worldrequire name=A.css html of Arequire name=B.css divhtml of Brequire name=C.css divhtml of C在页面的头部插入一个 html 注释“”作为占位, 而将原来字面书写的资源引用改成模板接口(r equi re)调用,该接口负责收集页面所需资 源。require接口实现非常简单,就是准备一个数组,收集资源引用,并且可以去重。最 后在页面输出的前一刻,我们将 require 在运行时收集到的“ A.css ”、“ B.css ”、“ C.css ” 三个资源拼接成 html 标签,替换掉注释占位“”, 从而得到我们需要的页面结构。经过 fis 团队的总结,我们发现模板层面只要实现三个开发接口,既可以比较完美的 实现目前遗留的大部分性能优化原则,这三个接口分别是:1. require(String id):收集资源加载需求的接口,参数是资源id。2. widget(String template_id):加载拆分成小组件模板的接口。你可以叫它为 load、component 或者 pagelet 之类的。总之,我们需要一个接口把一个大 的页面模板拆分成一个个的小部分来维护,最后在原来的大页面以组件为单位来 加载这些小部件。3. script(String code):收集写在模板中的js脚本,使之出现的页面底部,从而 实现性能优化原则中的“将 js 放在页面底部”原则。实现了这些接口之后,一个重构后的模板页面的源代码可能看起来就是这样的了:hello worldrequire name=jquery.jsrequire name=A/A.csswidget name=A/A.tplscriptconsole.log(A loaded)/scriptrequire name=B/B.csswidget name=B/B.tplrequire name=C/C.csswidget name=C/C.tpl 而最终在模板解析的过程中,资源收集与去重、页面 script 收集、占位符替换操作,最终从服务端发送出来的 html 代码为:hello worlddivhtmlofAdivhtmlofBdivhtmlofCscript type=text/javascript src=jquery.js/script script type=text/javascriptconsole.log(A loaded);/script/body/html不难看出,我们目前已经实现了“按需加载”,“将脚本放在底部”,“将样式表放在头部”三项优化原则。前面讲到静态资源在上线后需要添加 hash 戳作为版本标识,那么这种使用模板语言 来收集的静态资源该如何实现这项功能呢?答案是:静态资源依赖关系表。假设前面讲到的 模板源代码所对应的目录结构为下图所示:r_i A.essItaGBitplCjCCBS:C.tpt.;boatstnp.cu(icex.tpi卜 jquvryjs那么我们可以使用工具扫描整个 project 目录,然后创建一张资源表,同时记录每个 资源的部署路径,可以得到这样的一张表:res: A/A.css: uri: /A/A_1688c82.css,type: css,B/B.css: uri: /B/B_52923ed.css,type: css,C/C.css: uri: /C/C_6dda653.css,type: css,bootstrap.css: uri: bootstrap_08f2256.css,type: css,jquery.js: uri: jquery_9155343.css,type: js,pkg: 基于这张表,我们就很容易实现 require name=” id ” 这个模板接口了。只须查表即可。比如执行require name=” jquery.js ”,查表得到它的url是“/jquery_9151577.js ”,声明一个数组收集起来就好了。这样,整个页面执行完毕之后,收集资源加载需求,并替换页面的占位符,即可实现资源的 hash 定位,得到:hello worldlinkrel=stylesheet type=text/csshref=B/B 52923ed.csslinkrel=stylesheet type=text/csshref=C/C 6dda653.csshtmlofAhtmlofBhtmlofCscript type=text/javascript src=jquery_9155343.js/script script type=text/javascriptconsole.log(A loaded);/html接下来,我们讨论如何在基于表的设计思想上是如何实现静态资源合并的。或许有些团 队使用过 combo 服务,也就是我们在最终拼接生成页面资源引用的时候,并不是生成多 个独立的 link 标签,而是将资源地址拼接成一个 url 路径,请求一种线上的动态资源合 并服务,从而实现减少HTTP请求的需求,比如:htmlheadtitlehello world/titlelink rel=stylesheet type=text/css href=/combo?files=bootst rap_08f2256.css,A/A_1688c82.css,B/B_52923ed.css,C/C_6dda653.css /headbodydivhtml of A/divdivhtml of B/div divhtml of C/divscript type=text/javascript src=jquery_9155343.js/scriptscript type=text/javascriptconsole.log(A loaded);这个“/combo?files=file1,file2,file3,.的 url 请求响应就是动态 combo 服务提 供的,它的原理很简单,就是根据get请求的files参数找到对应的多个文件,合并成一 个文件来响应请求,并将其缓存,以加快访问速度。这种方法很巧妙,有些服务器甚至直接集成了这类模块来方便的开启此项服务,这种做 法也是大多数大型 web 应用的资源合并做法。但它也存在一些缺陷:1. 浏览器有 url 长度限制,因此不能无限制的合并资源。2. 如果用户在网站内有公共资源的两个页面间跳转访问,由于两个页面的 combo 的 url 不一样导致用户不能利用浏览器缓存来加快对公共资源的访问速度。对于上述第二条缺陷,可以举个例子来看说明:假设网站有两个页面A和B A页面使用了 a,b,c,d四个资源 B 页面使用了 a, b, e, f 四个资源如果使用 combo 服务,我们会得: A页面的资源引用为:/combo?files=a,b,c,dB 页面的资源引用为: /combo?files=a,b,e,f两个页面引用的资源是不同的url,因此浏览器会请求两个合并后的资源文件,跨 页面访问没能很好的利用 a、b 这两个资源的缓存。很明显,如果 combo 服务能聪明的知道 A 页面使用的资源引用为“/combo?files=a,b 和“/combo?files=c,d ,而 B 页面使用的资源引用为 “/combo?files=a,b ”,“/combo?files=e,f 就好了。这样当用户在访问A页面之后再 访问 B 页面时,只需要下载 B 页面的第二个 combo 文件即可,第一个文件已经在访问 A 页面时缓存好了的。基于这样的思考, fis 在资源表上新增了一个字段,取名为“ pkg ”,就是资源合并生 成的新资源,表的结构会变成:一A/A.css: uri: /A/A_1688c82.css, B/B.css: uri: /B/B_52923ed.css,type: css,C/C.css: uri: /C/C_6dda653.css,type: css,bootstrap.css: uri: bootstrap_08f2256.css,type: css,jquery.js: uri: jquery_9155343.css,type: js,pkg: p0:uri: /pkg/utils_b967346.css,type: css,has: bootstrap.css, A/A.css,p1:uri: /pkg/others_0d4552a.css,type: css,has: B/B.css, C/C.css相比之前的表,可以看到新表中多了一个 pkg 字段,并且记录了打包后的文件所包含 的独立资源。这样,我们重新设计一下r equi re n ame=” id ”这个模板接口:在查表的 时候,如果一个静态资源有 pkg 字段,那么就去加载 pkg 字段所指向的打包文件,否则 力口载资源本身。比如执行require name= bootstrap.css ”查表得知 bootstrap.css被打包在了“ p0 ”中,因此取出 p0 包的 url “/pkg/utils_b967346.css ”,并且记录页 面已加载了“ bootstrap.css ”和“ A/A.css ”两个资源。这样一来,之前的模板代码执行之 后得到的html就变成了:hello worldlink rel=stylesheet type=text/css href=pkg/others_0d4552a. csshtml of Ahtml of Bhtml of Cscript type=text/javascript src=jquery_9155343.js/scriptscript type=text/javascriptconsole.log(A loaded); css 资源请求数由原来的 4 个减少为 2 个。这样的打包结果是怎么来的呢?答案是 配置得到的。我们来看一下带有打包结果的资源表的fis配置:fis.config.set(pack, pkg/util.css: bootstrap.css, A/A.css,pkg/other.css: *.css);我们将“ bootstrap.css ”、“ A/A.css ”打包在一起,其他 css 另外打包,从而生成 两个打包文件,当页面需要打包文件中的资源时,模块框架就会收集并计算出最优的资源加 载结果,从而解决静态资源合并的问题。这样做的原因是为了弥补 combo 在前面讲到的两点技术上的不足而设计的。但也不 难发现这种打包策略是需要配置的,这就意味着维护成本的增加。但好在它有两个优势可以 一定程度上弥补这个问题:1. 打包的资源只是原来独立资源的备份。打包与否不会导致资源的丢失,最多是没有合并的很好而已。2. 配置可以由工程师根据经验人工维护,也可以由统计日志生成,这为性能优化自适应网站设计提供了非常好的基础。关于第二点,fis有这样辅助系统来支持自适应打包算法:至此,我们通过基于表的静态资源管理系统和三个模板接口实现了几个重要的性能优化 原则,现在我们再来回顾一下前面的性能优化原则分类表,剔除掉已经做到了的,看看还剩 下哪些没做到的:优化方向优化手段请求数量拆分初始化负载请求带宽拆分初始化负载缓存利用使Ajax可缓存页面结构尽早刷新文档的输出“拆分初始化负载”的目标是将页面一开始加载时不需要执行的资源从所有资源中分离 出来,等到需要的时候再加载。工程师通常没有耐心去区分资源的分类情况,但我们可以利 用组件化框架接口来帮助工程师管理资源的使用。还是从例子开始思考:hello worldrequire name=jquery.jsbutton id=myBtnClick Me/buttonscript$(#myBtn).click(function()var dialog = require(dialog/dialog.js); dialog.alert(you catch me!););/script在 fis 给百度内部团队开发的架构中,如果这样书写代码,页面最终的执行结果会变 成:hello worldbutton id=myBtnClick Mescript type=text/javascript src=/jquery_9151577.js/scriptscript type=text/javascript src=/dialog/dialog_ae8c228.js/sc riptscript type=text/javascript$(#myBtn ). click function ()var dialog = require(dialog/dialog.js);dialog.alert(you catch me!););/script!-SCRIPTS PLACEHOLDER-/body/htmlfis系统会分析页面中require(id)函数的调用,并将依赖关系记录到资源表对应资源 的 deps 字段中,从而在页面渲染查表时可以加载依赖的资源。但此时 dialog.js 是以 script 标签的形式同步加载的,这样会在页面初始化时出现资源的浪费。因此, fis 团队提 供了 require.async 的接口,用于异步加载一些资源,源码修改为:headtitlehello worldrequire name=jquery.js/headbutton id=myBtnClick Mescript$(#myBtn).click(function() require.async(dialog/dialog.js, function( dialog )dialog.alert(you catch me!);););/script这样书写之后,fis系统会在表里以async字段来标准资源依赖关系是异步的。fis提供的静态资源管理系统会将页面输出的结果修改为:hello worldbutton id=myBtnClick Mescript type=text/javascript src=/jquery_9151577.js/scriptscript type=text/javascript src=/dialog/dialog_ae8c228.js/sc riptscript type=text/javascript$(#myBtn ). click function ()require.async(dialog/dialog.js function( dialog )dialog.alert(you catch me!);););/script !SCRIPTS PLACEHOLDERdialog.js 不会在页面以 script src 的形式输出,而是变成了资源注册,这样,当页 面点击按钮触发require.async执行的时候,async函数才会查表找到资源的url并加 载它,加载完毕后触发回调函数。到目前为止,我们又以架构的形式实现了一项优化原则(拆分初始化负载),回顾我们 的优化分类表,现在仅有两项没能做到了:优化方向优化手段缓存利用使Ajax可缓存页面结构尽早刷新文档的输出剩下的两项优化原则要做到并不容易,真正可缓存的 Ajax 在现实开发中比较少见, 而尽早刷新文档的输出的情况 facebook 在 2010 年的 velocity 上提到过,就是 BigPipe 技术。当时 facebook 团队还讲到了 Quickling 和 PageCache 两项技术,其 中的PageCache算是比较彻底的实现Ajax可缓存的优化原则了。fis团队也曾与某产 品线合作基于静态资源表、模板组件化等技术实现了页面的 PipeLine 输出、以及 Quickling 和 PageCache 功能,但最终效果没有达到理想的性能优化预期,因此这两个 方向尚在探索中,相信在不久的将来会有新的突破。总结其实在前端开发工程管理领域还有很多细节值得探索和挖掘,提升前端团队生产力水平 并不是一句空话,它需要我们能对前端开发及代码运行有更深刻的认识,对性能优化原则有 更细致的分析与研究。 fis 团队一直致力于从架构而非经验的角度实现性能优化原则;解决 前端工程师开发、调试、部署中遇到的工程问题;提供组件化框架,提高代码复用率;提供 开发工具集,提升工程师的开发效率。在前端工业化开发的所有环节均有可节省的人力成本, 这些成本非常可观,相信现在很多大型互联网公司也都有了这样的共识。本文只是将这个领 域中很小的一部分知识的展开讨论,抛砖引玉,希望能为业界相关领域的工作者提供一些不 一样的思路。欢迎关注fis项目,对本文有任何意见或建议都可以在fis开源项目中进行反 馈和讨论。
展开阅读全文
相关资源
相关搜索

最新文档


当前位置:首页 > 图纸设计 > 毕设全套


copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!