随着业务的不断迭代,项目日渐壮大,为了给用户提供更优的体验,性能优化是前后端开发者避不开的话题。
他山之石可以攻玉,基于雅虎军规一十四条规范及日常开发中收集的经验,本文收录了网页性能优化策略,整个过程不似白云写《月子》般憋闷,有的只是每每的醍醐灌顶、温故知新。
回流(reflow) render树中一部分或全部元素需要改变尺寸、布局、或着需要隐藏而需要重新构建,这个过程叫做回流; 比如:添加或者删除可见的DOM元素,元素位置改变,元素尺寸改变——边距、填充、边框、宽度和高度
重绘(repaint) render树中一部分元素改变,而不影响布局的,只影响外观的,比如颜色。该过程叫做重绘,页面至少经历一次回流和重绘(第一次加载的时候) 比如:只有颜色改变的时候就只会发生重绘而不会引起回流
如何优化 就比如 display:none 这个属性,很多人忽略了它所带的回流性能开销 如果想设定元素的样式,通过改变元素的 class 类名 除此之外, 使用 Javascript 动态插入多个节点时, 可以使用documentFragment. 创建后一次插入. 就能避免多次的渲染性能.
DOM是页面元素对象的体现,每次寻找的时候,都会一层层的去寻找,对于相同且已经查找过的节点,每次都去重新找,如果节点层级关系多了,性能就很低了。
事件委托:也称为事件代理(Event Delegation)。是Javascript中常用绑定事件的常用技巧。“事件代理”即是把原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务。 为什么要使用事件委托:工作中会碰到需要大量事件处理函数的场景,如果批量添加事件处理函数,会导致监听数量太多,造成大量内存消耗。每个事件处理函数都是一个单独的引用类型,这些函数本身也会占用内存。
CSS animations, transforms 以及 transitions 不会自动开启GPU加速,很多浏览器提供了某些触发的CSS规则。Chrome, FireFox, Safari, IE9+和最新版本的Opera都支持硬件加速,当它们检测到页面中某个DOM元素应用了某些CSS规则时就会开启,最显著的特征的元素的3D变换。
虽然我们可能不想对元素应用3D变换,可我们一样可以开启3D引擎。例如我们可以用transform: translateZ(0); 来开启硬件加速 。
工作中遇到防抖的频率相当高,处理不当就会引起浏览器卡顿; 防抖:给定的时间内继续触发事件就会清除定时器然后重新开始计时,直到你在这个时间段内不再触发事件,才会执行func函数。
在网页中,我们可以看到有很多的小图标,比如微博上的登录位置有很多这样的小图标。如果将这些图标分别存在服务器上,那么当需要显示的时候将会发出很多次请求–>响应–>下载,这样一来将会消耗大量的时间来下载这些小图标; 所以为了提高网页响应速度,将这些小图片全部放到一张图片上,此图称为雪碧图/精灵图 精灵图片的使用难点在于如何在这一张图片中定位到我们需要的部分,首先我们需要理解精灵题坐标,左上角为原点,往上y值为负数,越来越小;往左x为负数,越来越小 假如我们截取第2列2排的皇冠2,此时精灵图往上移动,相当于y减小了40px(假设值),此时y坐标为-40px;往左移动24px,此时x坐标为-24px,所以如果我们需要获取vip6图标,需要如下设置;
去除不必要的空白符,格式符,注释符。 简写方法名,参数名压缩js脚本。
**懒加载:**如图片懒加载,Vue-Lazyload插件中的preLoad属性值可以调整懒加载滑动配置
按需加载也是重用的性能手段,当需要加载该路由时再去加载对应的路由资源
CDN:内容分发网络,意思就是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。 通俗来说:就是在离你最近的地方,放置一台性能好、链接顺畅的副本服务器,让你能够以最近的距离,最快的速度获取内容
Expires:response header里的过期时间,浏览器再次加载资源时,如果在这个过期时间内,则命中强缓存。设置以分钟为单位的绝对过期时间, 设置相对过期时间。
Cache-Control:当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。指明以秒为单位的缓存时间。
cache-control除了该字段外,还有下面几个比较常用的设置值:
- -no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
- -no-store:直接禁止浏览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
- -public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
- -private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
Expires优先级比Cache-Control低, 同时设置Expires和Cache-Control则后者生效.
HTTP压缩是在Web服务器和浏览器间传输压缩文本内容的方法。HTTP压缩采用通用的压缩算法如gzip等压缩HTML、Javascript或 CSS文件。压缩的最大好处就是降低了网络传输的数据量,从而提高客户端浏览器的访问速度。当然,同时也会增加一点点服务器的负担。Gzip是比较常见的 一种HTTP压缩算法。
HTTP:从客户端到服务器端的请求消息。包括消息首行中,对资源的请求方法资源的标识符以及使用的协议。 请求过程:当你打开网页的时候,你所看到的文字,图片,多媒体,这一切内容,都是你从服务器获取的,每一个内容的获取,就是一个http请求,可以采取合并css/js文件,合并图片如3.1,合并接口请求的方式进行优化。
定义: 原始请求被重新转向了其他请求。多了一次请求。 状态码:
- 301(Moved Permanently):被移动到了另外的位置。
- 302 Found:被找到了,不在原始位置,临时重定向。
关于如何避免重定向或者将重定向的影响降低到最小化通常有以下几种方法:
-
删除并非绝对必要的重定向 删除并非绝对必要的重定向,再通过其它的方式进行重定向。永远不要链接你已经知道的重定向的页面,永远不要访问经过多次重定向才能访问的资源。
-
结尾的斜线 通常,带有结尾带有斜线的URL表示目录,而没有带斜线的URL表示文件。 http://example.com/foo/表示目录。 http://example.com/foo表示文件。
-
清理重定向链接 除了需要删除重定向之外,还需要清理重定向链。将所有站点重定向从非www版本到www版 本,然后再重定向到https版本。例如键入“test.com”的用户重定向到“www.test.com”然后再重定向到“https:// www.test.com”,这种情况经常有发生。解决的方案是确保旧的全站点重定向不会从非www到www,而是从非www到https://www。
Last-Modify/If-Modify-Since:浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间;当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存
Etag/If-None-Match:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。If-None-Match:发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定是否命中协商缓存;
ETag和Last-Modified的作用和用法,他们的区别: 1.Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度; 2.在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值; 3.在优先级上,服务器校验优先考虑Etag。
1.Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度; 2.在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值; 3.在优先级上,服务器校验优先考虑Etag。
这样会先加载css的样式,在渲染dom的时候已经知道了自己的样式了,所以一次渲染即可成功,这样可以防止闪跳、白屏或者布局混乱的现象发生。 如果css放在底部,那么需要先渲染dom,然后加载css后会重新渲染之前的dom,这就需要两次渲染,用户体验较差。
现在浏览器为了更好的用户体验,渲染引擎会尝试尽快在屏幕上显示内容,它不会等到所有的HTMl元素解析之后在构建和布局dom树,所以部分内容将被解析并显示。也就是说浏览器能够渲染不完整的dom树和cssom,尽快的减少白屏的时间。
当 <script>、<img>、<iframe> 标签的 src 属性为空时,浏览器在渲染的过程中仍会将 src 属性中的空内容进行加载,直至加载失败,这样就阻塞了页面中其他资源的下载进程,而且最终加载到的内容是无效的,因此要尽量避免。
src是 source 的缩写,指向外部资源的位置,指向的内容会嵌入到文档中当前标签所在的位置;在请求 src 资源时会将其指向的资源下载并应用到文档内,比如 img 图片,js 脚本等。当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载执行完毕。这也是为什么要将 js 脚本放在底部而不是头部的原因
显而易见,是为了减少图片请求且字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。 使用代码代替可以描述的任何低成本图片效果。
v-show 是通过控制display属性来进行DOM的显示与隐藏,主要用于频繁操作; v-if 是真正意义上的条件渲染(销毁和创建元素),条件为true时创建DOM,条件为false时销毁DOM,主要用于大量数据渲染到页面(符合条件就将数据渲染),频繁使用会消耗性能。
性能区别:
- v-if有更高的切换开销,v-show有更高的初始渲染开销。 如果需要频繁的切换,使用v-show比较好,如果运行条件很少改变,使用v-if比较好。
- v-show比v-if性能更高,因为v-show只能动态的改变样式,不需要增删DOM元素。
- v-if切换时候会实时的销毁和重建内部的事件、钩子函数等,v-show只会初始化渲染时候执行,再切换时候不会执行生命后期的过程。
computed:根据已有的属性生成新的属性 计算属性函数是通过函数将结果作为计算属性的值,把该计算属性挂在vm上,在模板中不能直接调用函数。计算属性的结果会被缓存,初始化时调用生成初始值,只有计算所依赖的数据发生变化才会重新计算。 当dirty=true时依赖的数据发生变化说明需要重新计算计算属性的返回值 当dirty=false时说明计算属性的值没有变,不需要重新计算,节省内存开销 当计算属性中的内容发生变化后,计算属性的Watcher与组件的Watcher都会得到通知。 计算属性的Watcher会将自己的dirty属性设置为true,组件的Watcher也会收到通知,从而执行render函数进行重新渲染操作,重新读取计算属性的值,此时计算属性的Wather已经把自己的dirty属性设置为true,所以会重新计算计算属性的值
watch的属性:可以监视data和computed里的已有属性 vue实例将会在实例化时调用vm.$watch()遍历watch对象的每一个属性,vm.$watch实际上是对Watcher的一种封装。
Object.freeze方法是es5中新增加的一个属性描述符,用于锁定一个对象,被锁定后的对象将不可添加或删除属性,对自身已有属性也不可进行修改,也就是不进行数据劫持,节省内存开销。 另外,freeze冻结的是堆内存中的值,和栈中的引用无关。
keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 transition 相似,keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件链中。 组件切换过程中 把切换出去的组件保留在内存中,防止重复渲染DOM,减少加载时间及性能消耗,提高用户体验性
原理: 在created钩子函数调用时将需要缓存的 VNode 节点保存在 this.cache 中/在 render(页面渲染) 时,如果 VNode 的 name 符合缓存条件(可以用 include 以及 exclude 控制),则会从 this.cache 中取出之前缓存的 VNode实例进行渲染。
参数(Props)
include - 字符串或正则表达式。只有名称匹配的组件会被缓存。 exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。 max - 数字。最多可以缓存多少组件实例 对生命周期函数变化
被包含在 keep-alive 中创建的组件,会多出两个生命周期的钩子: activated 与 deactivated
-
activated:在 keep-alive 组件激活时调用
-
deactivated:在 keep-alive 组件离开时调用
key:
- 辅助跟踪每个节点的身份,从而重用和重新排序现有的元素;
- 理想的key值是每项都有且唯一的id,“data.id”,且不能为index
假如我们存在一个ul如下,然后删除第二个元素,diff简略版分析过程如下
原数组创建一个虚拟节点,虚拟节点里面放着js对象描述的节点,里面放着简略版的重要的属性信息, 然后虚拟dom再创建真实的dom显示显示在页面上。 当你更新了要遍历的数组,这个数组并不是重新去遍历,而是新的数组也会创建一个虚拟的dom,然后这个新的虚拟的dom会跟原来的虚拟dom进行对比,查出来不同的点再去进行相应的更新,创建最终的dom。
**那这个相应的更新是怎么一个原理呢:**新的虚拟dom和旧的虚拟dom对比的时候,如果想要进行旧的节点复用和重新排序是需要给每一个数组元素增加一个“唯一标识这个数组内容的key值”,比如说“111”文本唯一的标识可以是id=1,“222”为id=2,“333”为id=3,这样在新数组删除“222”的时候,“111”和“333”唯一的标识还是id=1和id=3,然后Vue在进行虚拟dom对比时,就会让新旧的id=1的“111”进行对比,没有改变就复用内容,对比id=2的“222”时,发现新的虚拟dom中没有id=2的值,那Vue就标记id=2的值被删除了,然后再对比id=3的值,发现id=3的新旧虚拟dom也是一样的,然后也会复用,所以Vue就是根据每个数组内容都有唯一标识的key来进行智能的判断是哪个位置上的哪个内容有了什么变化(删除,修改等),进而通过“重用和重新排序现有元素”来实现元素更新,最终遍历生成最终真实dom。
diff算法详细过程请看本人的另一篇文章
Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件
而对于定时器、addEventListener 注册的监听器等,就需要在组件销毁的生命周期钩子中手动销毁或解绑,以避免内存泄露
npm run build编译之后,我们查看编译生成的文件,发现有很多.map文件,这些文件占了不小的空间。.map文件的作用是帮助编译后的代码调试,但是我们上线的代码已经调试完成,所以上线时可以不生成.map文件
项目内可适当进行代码合并,可减少分包,从而减少请求,按照以下配置规则可进行自检,查看是否有冗余的分包规则; vue.config.js分包配置规则示例如下:
参考文献webpack split chunks官网
SSR全拼是Server-Side Rendering,服务端渲染。 所谓服务端渲染,指的是把vue组件在服务器端渲染为组装好的HTML字符串,然后将它们直接发送到浏览器,最后需要将这些静态标记混合在客户端上完全可交互的应用程序。 从这张图片,我们可以知道:我们需要通过Webpack打包生成两份bundle文件: Client Bundle,给浏览器用。和纯Vue前端项目Bundle类似 Server Bundle,供服务端SSR使用,一个json文件
左侧Source部分就是我们所编写的源代码,所有代码有一个公共入口,就是app.js,紧接着就是服务端的入口(entry-server.js)和客户端的入口(entry-client.js)。当完成所有源代码的编写之后,我们通过webpack的构建,打包出两个bundle,分别是server bundle和client bundle;当用户进行页面访问的时候,先是经过服务端的入口,将vue组件组装为html字符串,并混入客户端所访问的html模板中,最终就完成了整个ssr渲染的过程。
SSR能够在服务端先进行请求渲染,由于服务端进行请求数据的时延较小,能够快速拿到数据并且返回HTML代码。在客户端可以直接渲染数据而不需要花费一些请求数据的时间,这是服务端渲染的好处。返回内容SSR会比普通的SPA在HTML代码中多出首次渲染的结果,这样在初始化的时候直接将页面进行渲染,无需花费时间去请求数据再次渲染。 SSR并不是说只在服务端进行渲染,而是说SSR会比普通的客户端渲染多一次在服务端渲染。到浏览器这边,SSR还是需要进行再次初始化Vue,并且经过beforeCreate、created、beforeMount、mounted生命周期,但是在客户端VNode进行patch的时候,如果遇到服务端渲染过的节点,那么会跳过,所以在浏览器端渲染的时候可以减少一些工作,从而提高了页面体验。
传统的SPA模式 即客户端渲染的模式:Vue.js构建的应用程序,默认情况下是有一个html模板页,然后通过webpack打包生成一堆js、css等等资源文件。然后塞到index.html中
用户输入url访问页面 -> 先得到一个html模板页 -> 然后通过异步请求服务端数据 -> 得到服务端的数据 -> 渲染成局部页面 -> 用户
SSR模式:即服务端渲染模式
以下为启动性能优化
使用 分包加载 是优化小程序启动耗时效果最明显的手段。建议开发者按照功能划分,将小程序的页面按使用频率和场景拆分成不同分包,实现代码包的按需加载。
分包加载具有以下优势: 承载更多功能:小程序单个代码包的体积上限为 2M,使用分包可以提升小程序代码包总体积上限,承载更多的功能与服务。 降低代码包下载耗时:使用分包后可以显著减少启动时需要下载的代码包大小,在不影响功能正常使用的前提下,有效降低启动耗时。 降低小程序代码注入耗时:若未开启按需注入,小程序编译时会将所有 js 文件打包成同一个文件一次性的注入,并执行所有页面和自定义组件的代码。分包后可以降低注入和实际执行的代码量,从而降低注入耗时。 降低页面渲染耗时:使用分包可以避免不必要的组件和页面初始化。 降低内存占用:分包能够实现页面、组件和逻辑较粗粒度的按需加载,从而降低内存的占用。
此外,结合分包加载的几个扩展功能,可以进一步优化启动耗时: 8.1.1 独立分包 小程序中的某些场景(如广告页、活动页、支付页等),通常功能不是很复杂且相对独立,对启动性能有很高的要求。独立分包可以独立于主包和其他分包运行。从独立分包页面进入小程序时,不需要下载主包。建议开发者将部分对启动性能要求很高的页面放到特殊的独立分包中。
8.1.2 分包预下载 在使用「分包加载」后,虽然能够显著提升小程序的启动速度,但是当用户在使用小程序过程中跳转到分包内页面时,需要等待分包下载完成后才能进入页面,造成页面切换的延迟,影响小程序的使用体验。分包预下载便是为了解决首次进入分包页面时的延迟问题而设计的。
独立分包和分包预下载可以配合使用,获得更好的效果,详情请参考 独立分包与分包预下载教程
1.3 分包异步化 「分包异步化」将小程序的分包从页面粒度细化到组件甚至文件粒度。这使得本来只能放在主包内页面的部分插件、组件和代码逻辑可以剥离到分包中,并在运行时异步加载,从而进一步降低启动所需的包大小和代码量 分包异步化能有效解决主包大小过度膨胀的问题。
在 app.json 中通过 usingComponents 全局引用的自定义组件和通过 plugins 全局引入的插件,会在小程序启动时随主包一起下载和注入 JS 代码,影响启动耗时。
即使扩展库和部分官方插件不占用主包大小,但是启动时仍然需要下载和注入 JS 代码,对启动耗时的影响和其他插件并没有区别。
- 如果自定义组件只在某个分包的页面中使用,应定义在页面的配置文件中
- 全局引入的自定义组件会被认为是所有分包、所有页面都需要的,会影响「按需注入」的效果和小程序代码注入的耗时。
- 如果插件只在某个分包的中使用,请仅在分包中引用插件
- 例如:很多小程序会用到「小程序直播」插件,但是直播功能通常不在主包页面中使用或较为低频,此时建议通过分包引入「小程序直播」插件。
- 如果确实需要在主包中或被多个分包使用的插件,仍可以考虑将插件置于一个分包,并通过「分包异步化」的形式异步引入。
小程序代码包在下载时会使用 ZSTD 算法进行压缩,图片、音频、视频、字体等资源文件会占用较多代码包体积,并且通常难以进一步被压缩,对于下载耗时的影响比代码文件大得多。
建议开发者在代码包内的图片一般应只包含一些体积较小的图标,避免在代码包中包含或在 WXSS 中使用 base64 内联过多、过大的图片等资源文件。这类文件应尽可能部署到 CDN,并使用 URL 引入。
除了工具默认忽略或开发者明确声明忽略的文件外,小程序打包会将工程目录下所有文件都打入代码包内。意外引入的第三方库、版本迭代中被废弃的代码或依赖、产品环境不需要的测试代码、未使用的组件、插件、扩展库,这些没有被实际使用到的文件和资源也会被打入到代码包里,从而影响到代码包的大小。
建议使用微信开发者工具提供的「代码静态依赖分析」,不定期地分析代码包的文件构成和依赖关系,以此优化代码包大小和内容。对于仅用于本地开发调试,不应包含在小程序代码包的文件,可以使用工具设置的 packOptions.ignore 配置忽略规则。
在使用打包工具(如 Webpack、Rollup 等)对小程序代码进行预处理时,可以利用 tree-shaking 等特性去除冗余代码,也要注意防止打包时引入不需要的库和依赖。
小程序代码注入的优化可以从优化代码量和优化执行耗时两个角度着手。
页面首屏渲染的优化,目的是让「首页渲染完成」(Page.onReady) 尽可能提前。但很多情况下「首页渲染完成」可能还是空白页面,因此更重要的是让用户能够更早的看到页面内容(First Paint 或 First Contentful Paint)。
小程序的运行时性能直接决定了用户在使用小程序功能时的体验。如果运行时性能出现问题,很容易出现页面滚动卡顿、响应延迟等问题,影响用户使用。如果内存占用过高,还会出现黑屏、闪退等问题。 在优化运行时性能前,建议开发者先了解下小程序的运行环境和运行机制。 开发者可以从以下方面着手进行启动性能的优化:
- 合理使用 setData
- 渲染性能优化
- 页面切换优化
- 资源加载优化
- 内存优化