GimmeS 2019-12-15
性能优化,一直作为前端的一个热点问题,作为一个优秀的前端开发人员,性能优化时必备技能。本文将从减少http请求次数、减少单次请求资源大小、渲染优化、资源加载优化等四个大方向,下分诸多小方向,全面总结常用前端优化方法。
(内容较多请看目录)
浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:
Push Cache:是指 HTTP2 在 server push 阶段存在的缓存。Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
CDN 的核心点有两个,一个是缓存,一个是回源。
“缓存”就是说我们把资源 copy一份到CDN服务器上这个过程,“回源”就是说CDN发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。
CDN往往被用来存放静态资源。所谓“静态资源”,就是像 JS、CSS、图片等不需要业务服务器进行计算即得的资源。用户可以从一个较优的服务器获取数据,从而达到快速访问,并减少源站负载压力的目的。
另外,CDN的域名必须和主业务服务器的域名不一样,要不,同一个域名下面的Cookie各处跑,浪费了性能流量的开销,CDN域名放在不同的域名下,可以完美地避免了不必要的 Cookie 的出现!
将公共的js、css样式合并为一个大文件。
根据不同页面的需求单独合并所需js、css文件。
尽量避免使用重定向,当页面发生了重定向,就会延迟整个HTML文档的传输。在HTML文档到达之前,页面中不会呈现任何东西,也没有任何组件会被下载,降低了用户体验。
如果一定要使用重定向,如http重定向到https,要使用301永久重定向,而不是302临时重定向。因为,如果使用302,则每一次访问http,都会被重定向到https的页面。而永久重定向,在第一次从http重定向到https之后,每次访问http,会直接返回https的页面。
css压缩,就是进行简单的压缩,压缩空白等。
图片压缩,主要也是减小体积,在不影响观感的前提下,可以删除一些无关紧要的色彩。另外可以使用webp格式图片。
gzip压缩主要是针对html文件来说的,它可以将html中重复的部分进行一个打包,多次复用。
js混淆可以有简单的压缩(将空白字符删除)、丑化(将一些变量缩小)、或者对js进行混淆加密。
CSS 选择符是从右到左进行匹配的,比如 #myul li {}实际开销相当高。因此需要对选择符进行优化,主要有如下几方面:
减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素。
重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。
重绘不一定导致回流,回流一定会导致重绘。
从上面可以知道,DOM改变容易引起回流和重绘,因此我们要减少DOM操作。
例子剖析,如下代码:
for(var count=0;count<10000;count++){ document.getElementById('container').innerHTML+='<span>我是一个小测试</span>' //我们每一次循环都调用 DOM 接口重新获取了一次 container 元素,额外开销 }
进化一
// 只获取一次container let container = document.getElementById('container') for(let count=0;count<10000;count++){ container.innerHTML += '<span>我是一个小测试</span>' }
进化二
考虑JS 的运行速度,比 DOM 快得多这个特性。我们减少 DOM 操作的核心思路,就是让 JS 去给 DOM 分压。
//减少不必要的DOM更改 let container = document.getElementById('container') let content = '' for(let count=0;count<10000;count++){ // 先对内容进行操作 content += '<span>我是一个小测试</span>' } // 内容处理好了,最后再触发DOM的更改 container.innerHTML = content
进化三
在 DOM Fragment 中,DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当做一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的XML片段。因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会引起 DOM 树的重新渲染的操作(reflow),且不会导致性能等问题。
let container = document.getElementById('container') // 创建一个DOM Fragment对象作为容器 let content = document.createDocumentFragment() for(let count=0;count<10000;count++){ // span此时可以通过DOM API去创建 let oSpan = document.createElement("span") oSpan.innerHTML = '我是一个小测试' // 像操作真实DOM一样操作DOM Fragment对象 content.appendChild(oSpan) } // 内容处理好了,最后再触发真实DOM的更改 container.appendChild(content)
进化四
当涉及到过万调数据进行渲染,而且要求不卡住画面,如何解决?
如何在不卡住页面的情况下渲染数据,也就是说不能一次性将几万条都渲染出来,而应该一次渲染部分 DOM,那么就可以通过 requestAnimationFrame 来每 16 ms 刷新一次。
setTimeout(() => { // 插入十万条数据 const total = 100000 // 一次插入 20 条,如果觉得性能不好就减少 const once = 20 // 渲染数据总共需要几次 const loopCount = total / once let countOfRender = 0 let ul = document.querySelector('ul') function add() { // 优化性能,插入不会造成回流 const fragment = document.createDocumentFragment() for (let i = 0; i < once; i++) { const li = document.createElement('li') li.innerText = Math.floor(Math.random() * total) fragment.appendChild(li) } ul.appendChild(fragment) countOfRender += 1 loop() } function loop() { if (countOfRender < loopCount) { window.requestAnimationFrame(add) } } loop() }, 0)
事件委托是指将事件监听器注册在父级元素上,由于子元素的事件会通过事件冒泡的方式向上传播到父节点,因此,可以由父节点的监听函数统一处理多个子元素的事件。利用事件委托,可以减少内存使用,提高性能及降低代码复杂度。
当用户进行滚动,触发scroll事件,用户的每一次滚动都将触发我们的监听函数。函数执行是吃性能的,频繁地响应某个事件将造成大量不必要的页面计算。因此,我们需要针对那些有可能被频繁触发的事件作进一步地优化。节流与防抖就很有必要了!
//节流函数 function throttle(fn,time){ var last = 0; return function(){ var context = this; var now = Date.now(); if (now - last >= time){ fn.apply(this, arguments); last = now; } }; } //防抖函数 function debounce(fn, time){ return function(){ var context = this; clearTimeout(timeId); timeId = setTimeout(function(){ fn.apply(context, arguements); }, time); }; }
资源懒加载和资源预加载都是一种错峰操作,在浏览器忙碌的时候不做操作,浏览器空间时,再加载资源,优化了网络性能。