从输入 URL 到页面加载完成的过程:javascript
咱们从输入 URL 到显示页面这个过程当中,涉及到网络层面的,有三个主要过程:html
对于 DNS 解析和 TCP 链接两个步骤,咱们前端能够作的努力很是有限。相比之下,HTTP 链接这一层面的优化才是咱们网络优化的核心。前端
HTTP 优化有两个大的方向:java
浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列以下:算法
MemoryCache,是指存在内存中的缓存。从优先级上来讲,它是浏览器最早尝试去命中的一种缓存。从效率上来讲,它是响应速度最快的一种缓存。浏览器秉承的是“节约原则”,咱们发现,Base64 格式的图片,几乎永远能够被塞进 memory cache,这能够视做浏览器为节省渲染开销的“自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的概率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们每每被直接甩进磁盘。后端
Service Worker 是一种独立于主线程以外的 Javascript 线程。它脱离于浏览器窗体,所以没法直接访问 DOM。这样独立的个性使得 Service Worker 的“我的行为”没法干扰页面的性能,这个“幕后工做者”能够帮咱们实现离线缓存、消息推送和网络代理等功能。咱们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。浏览器
它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的状况下,才会走协商缓存。缓存
对一条http get 报文的基本缓存处理过程包括7个步骤:服务器
图片描述网络
强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通讯。
是否足够新鲜时期:
经过 Expires: XXXX XXX XXX GMT (绝对日期时间,http/1.0) 或者 Cache-Control:max-age=XXXX (相对日期时间,http/1.1)在文档标明过时日期。
Cache-Control 相对于 expires 更加准确,它的优先级也更高。当 Cache-Control 与 expires 同时出现时,咱们以 Cache-Control 为准。
public 与 private 是针对资源是否可以被代理服务缓存而存在的一组对立概念。若是咱们为资源设置了 public,那么它既能够被浏览器缓存,也能够被代理服务器缓存;若是咱们设置了 private,则该资源只能被浏览器缓存。private 为默认值。
no-store与no-cache,no-cache 绕开了浏览器:咱们为资源设置了 no-cache 后,每一次发起请求都不会再去询问浏览器的缓存状况,而是直接向服务端去确认该资源是否过时(即走咱们下文即将讲解的协商缓存的路线)。no-store 比较绝情,顾名思义就是不使用任何缓存策略。在 no-cache 的基础上,它连服务端的缓存确认也绕开了,只容许你直接向服务端发送请求、并下载完整的响应。
协商缓存依赖于服务端与浏览器之间的通讯。协商缓存机制下,浏览器须要向服务器去询问缓存的相关信息,进而判断是从新发起请求、下载完整的响应,仍是从本地获取缓存的资源。若是服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种状况下网络请求对应的状态码是 304。
协商缓存的实现:从 Last-Modified 到 Etag,详细本身百度,这里再也不详细展开。
图片描述
当咱们的资源内容不可复用时,直接为 Cache-Control 设置 no-store,拒绝一切形式的缓存;不然考虑是否每次都须要向服务器进行缓存有效确认,若是须要,那么设 Cache-Control 的值为 no-cache;不然考虑该资源是否能够被代理服务器缓存,根据其结果决定是设置为 private 仍是 public;而后考虑该资源的过时时间,设置对应的 max-age 和 s-maxage 值;最后,配置协商缓存须要用到的 Etag、Last-Modified 等参数。
Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。
CDN 的核心点有两个,一个是缓存,一个是回源。
“缓存”就是说咱们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现本身没有这个资源(通常是缓存的数据过时了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。
CDN 每每被用来存放静态资源。所谓“静态资源”,就是像 JS、CSS、图片等不须要业务服务器进行计算即得的资源。而“动态资源”,顾名思义是须要后端实时动态生成的资源,较为常见的就是 JSP、ASP 或者依赖服务端渲染获得的 HTML 页面。
那“非纯静态资源”呢?它是指须要服务器在页面以外做额外计算的 HTML 页面。具体来讲,当我打开某一网站以前,该网站须要经过权限认证等一系列手段确认个人身份、进而决定是否要把 HTML 页面呈现给我。这种状况下 HTML 确实是静态的,但它和业务服务器的操做耦合,咱们把它丢到CDN 上显然是不合适的。
另外,CDN的域名必须和主业务服务器的域名不同,要不,同一个域名下面的Cookie各处跑,浪费了性能流量的开销,CDN域名放在不一样的域名下,能够完美地避免了没必要要的 Cookie 的出现!
在计算机中,像素用二进制数来表示。不一样的图片格式中像素与二进制位数之间的对应关系是不一样的。一个像素对应的二进制位数越多,它能够表示的颜色种类就越多,成像效果也就越细腻,文件体积相应也会越大。
一个二进制位表示两种颜色(0|1 对应黑|白),若是一种图片格式对应的二进制位数有 n 个,那么它就能够呈现 2^n 种颜色。
对于一张 100 100 像素的图片来讲,图像上有 10000 个像素点,若是每一个像素的值是 RGBA 存储的话,那么也就是说每一个像素有 4 个通道,每一个通道 1 个字节(8 位 = 1个字节),因此该图片大小大概为 39KB(10000 1 * 4 / 1024)。
可是在实际项目中,一张图片可能并不须要使用那么多颜色去显示,咱们能够经过减小每一个像素的调色板来相应缩小图片的大小。
了解了如何计算图片大小的知识,那么对于如何优化图片,想必你们已经有 2 个思路了:
JPEG/JPG 特色:有损压缩、体积小、加载快、不支持透明,JPG 最大的特色是有损压缩。这种高效的压缩算法使它成为了一种很是轻巧的图片格式。另外一方面,即便被称为“有损”压缩,JPG的压缩方式仍然是一种高质量的压缩方式:当咱们把图片体积压缩至原有体积的 50% 如下时,JPG 仍然能够保持住 60% 的品质。但当它处理矢量图形和 Logo 等线条感较强、颜色对比强烈的图像时,人为压缩致使的图片模糊会至关明显。
PNG 特色:无损压缩、质量高、体积大、支持透明,PNG(可移植网络图形格式)是一种无损压缩的高保真的图片格式。8 和 24,这里都是二进制数的位数。按照咱们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的能够呈现约 1600 万种颜色。PNG 图片具备比 JPG 更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。它弥补了上文咱们提到的 JPG 的局限性,惟一的 BUG 就是体积太大。
SVG 特色:文本文件、体积小、不失真、兼容性好,SVG(可缩放矢量图形)是一种基于 XML 语法的图像格式。它和本文说起的其它图片种类有着本质的不一样:SVG 对图像的处理不是基于像素点,而是是基于对图像的形状描述。
Base64 特色:文本文件、依赖编码、小图标解决方案,Base64 并不是一种图片格式,而是一种编码方式。Base64 和雪碧图同样,是做为小图标解决方案而存在的。
WebP 特色:年轻的全能型选手,WebP 像 JPEG 同样对细节丰富的图片信手拈来,像 PNG 同样支持透明,像 GIF 同样能够显示动态图片——它集多种图片文件格式的优势于一身。可是毕竟年轻,兼容性存在一些问题。
在客户端渲染模式下,服务端会把渲染须要的静态文件发送给客户端,客户端加载过来以后,本身在浏览器里跑一遍 JS,根据 JS 的运行结果,生成相应的 DOM。页面上呈现的内容,你在 html 源文件里里找不到——这正是它的特色。
在服务端渲染的模式下,当用户第一次请求页面时,由服务器把须要的组件或页面渲染成HTML字符串,而后把它返回给客户端。页面上呈现的内容,咱们在 html 源文件里也能找到。服务端渲染解决了一个很是关键的性能问题——首屏加载速度过慢,也解决了SEO搜索引擎的问题。
浏览器的渲染机制通常分为如下几个步骤:
在渲染DOM的时候,浏览器所作的工做其实是:
CSS 选择符是从右到左进行匹配的,好比 #myList li {}实际开销至关高。
CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程当中,不会渲染任何已处理的内容。即使 DOM 已经解析完毕了,只要 CSSOM 不 OK,那么渲染这个事情就不 OK。咱们将 CSS 放在 head 标签里 和尽快 启用 CDN 实现静态资源加载速度的优化。
JS 引擎是独立于渲染引擎存在的。咱们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。
重绘不必定致使回流,回流必定会致使重绘。回流比重绘作的事情更多,带来的开销也更大。在开发中,要从代码层面出发,尽量把回流和重绘的次数最小化。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>DOM操做测试</title> </head> <body> <div id="container"></div> </body> </html>
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>' }
进化二:
//减小没必要要的DOM更改 let container = document.getElementById('container') let content = '' for(let count=0;count<10000;count++){ // 先对内容进行操做 content += '<span>我是一个小测试</span>' } // 内容处理好了,最后再触发DOM的更改 container.innerHTML = content
事实上,考虑JS 的运行速度,比 DOM 快得多这个特性。咱们减小 DOM 操做的核心思路,就是让 JS 去给 DOM 分压。
在 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 刷新一次。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Document</title> </head> <body> <ul> 控件 </ul> <script> 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) </script> </body> </html>
window.requestAnimationFrame() 方法告诉浏览器您但愿执行动画并请求浏览器在下一次重绘以前调用指定的函数来更新动画。该方法使用一个回调函数做为参数,这个回调函数会在浏览器重绘以前调用。
注意:若您想要在下次重绘时产生另外一个动画画面,您的回调例程必须调用 requestAnimationFrame()。
咱们先了解javascript运行机制,对渲染是大有帮助的,能够看我历史文章JavaScript运行机制,
Javascript运行机制深刻。
事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。
常见的 macro-task 好比: setTimeout、setInterval、 setImmediate、script(总体代码)、 I/O 操做、UI 渲染等。
常见的 micro-task 好比: process.nextTick、Promise、MutationObserver 等。
例子分析:
// task是一个用于修改DOM的回调 setTimeout(task, 0)
上面代码,如今 task 被推入的 macro 队列。但由于 script 脚本自己是一个 macro 任务,因此本次执行完 script 脚本以后,下一个步骤就要去处理 micro 队列了,再往下就去执行了一次 render,必须等待下一次的loop。
Promise.resolve().then(task)
上面代码,咱们结束了对 script 脚本的执行,是否是紧接着就去处理 micro-task 队列了?micro-task 处理完,DOM 修改好了,紧接着就能够走 render 流程了——不须要再消耗多余的一次渲染,不须要再等待一轮事件循环,直接为用户呈现最即时的更新结果。
当咱们须要在异步任务中实现 DOM 修改时,把它包装成 micro 任务是相对明智的选择。
上面说了重绘与回流,Event loop,但不少人不知道的是,重绘和回流其实和 Event loop 有关。
当用户进行滚动,触发scroll事件,用户的每一次滚动都将触发咱们的监听函数。函数执行是吃性能的,频繁地响应某个事件将形成大量没必要要的页面计算。所以,咱们须要针对那些有可能被频繁触发的事件做进一步地优化。节流与防抖就颇有必要了!
详细看历史文章防抖动与节流