你们是从何时接触性能优化的呢?javascript
第一时间想到的又是什么呢?雅虎军规 ? 高性能javascript ?css
性能优化没有标准答案,咱们只能不断地把从搜索引擎和书中的知识付诸实践,这个过程是漫长且艰辛的html
本文为总结记录学习修言大佬小册,感兴趣的同窗能够购买支持正版前端
从输入 URL 到页面加载完成,发生了什么?java
归纳来讲,分为5步node
每一步均可以说的很细,这5个过程就是提升前端性能的根本的切入点react
关于第一第二步,咱们前端能作的很是有限,理解为主,webpack
DNS 解析花时间,能不能尽可能减小解析次数或者把解析前置?能——浏览器 DNS 缓存和 DNS prefetchgit
TCP 每次的三次握手都急死人,有没有解决方案?有——长链接、预链接、接入 SPDY 协议es6
为了知其因此然(应对深究的面试官),咱们了解下DNS的解析过程:
HTTP 链接这一层面的优化才是咱们网络优化的核心,
- 减小http请求次数
- 减小单次请求花费的时间
减小h t tp请求次数就一定会增长单次请求的开销,两点怎么权衡?
减小单次请求花费的时间
最多见操做就是资源的压缩和合并,该操做最多见的工具就是webpack, 因此问题就指向了webpack的性能瓶颈
构建过程时间太长
打包体积太大
从 webpack v4.0.0 开始,能够不用引入一个配置文件。然而,webpack 仍然仍是高度可配置的。
以 babel-loader 为例:
下面直接贴webpack官网的描述
babel-loader 很慢!
确保转译尽量少的文件。你可能使用
/\.js$/
来匹配,这样也许会去转译node_modules
目录或者其余不须要的源代码。要排除
node_modules
,参考文档中的loaders
配置的exclude
选项。你也能够经过使用
cacheDirectory
选项,将 babel-loader 提速至少两倍。 这会将转译的结果缓存到文件系统中。
babel 对一些公共方法使用了很是小的辅助代码,好比
_extend
。 默认状况下会被添加到每个须要它的文件中你能够引入 babel runtime 做为一个独立模块,来避免重复引入。
下面的配置禁用了 babel 自动对每一个文件的 runtime 注入,而是引入
babel-plugin-transform-runtime
而且使全部辅助代码从这里引用。
rules: [ // 'transform-runtime' 插件告诉 babel 要引用 runtime 来代替注入。 { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader?cacheDirectory=true', options: { presets: ['@babel/preset-env'], plugins: ['@babel/transform-runtime'] } } } ]
打包第三方依赖推荐 DllPlugin
DllPlugin 是基于 Windows 动态连接库(dll)的思想被创做出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。这个依赖库不会跟着你的业务代码一块儿被从新打包,只有当依赖自身发生版本变化时才会从新打包
Dll.config.js
// 以一个基于 React 的简单项目为例,咱们的 dll 的配置文件能够编写以下 const path = require('path') const webpack = require('webpack') module.exports = { entry: { // 依赖的库数组 vendor: [ 'prop-types', 'babel-polyfill', 'react', 'react-dom', 'react-router-dom', ] }, output: { path: path.join(__dirname, 'dist'), filename: '[name].js', library: '[name]_[hash]', }, plugins: [ new webpack.DllPlugin({ // DllPlugin的name属性须要和libary保持一致 name: '[name]_[hash]', path: path.join(__dirname, 'dist', '[name]-manifest.json'), // context须要和webpack.config.js保持一致 context: __dirname, }), ], }
运行这个配置文件,咱们的 dist 文件夹里会出现这样两个文件:
- vendor-manifest.json: 用于描述每一个第三方库对应的具体路径
- vendor.js: 咱们第三方库打包的结果
Webpack.config.js
const path = require('path'); const webpack = require('webpack') module.exports = { mode: 'production', // 编译入口 entry: { main: './src/index.js' }, // 目标文件 output: { path: path.join(__dirname, 'dist/'), filename: '[name].js' }, // dll相关配置 plugins: [ new webpack.DllReferencePlugin({ context: __dirname, // manifest就是咱们第一步中打包出来的json文件 manifest: require('./dist/vendor-manifest.json'), }) ] }
webpack因为是node编写的,node是单线程的,就算此刻存在多个任务,你也只能排队一个接一个地等待处理, 为了充分利用多核cpu的资源,根据cpu的核数,咱们能够fork多个进程, Happypack就是为此而生
const HappyPack = require('happypack') // 手动建立进程池 const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }) module.exports = { module: { rules: [ ... { test: /\.js$/, // 问号后面的查询参数指定了处理这类文件的HappyPack实例的名字 loader: 'happypack/loader?id=happyBabel', ... }, ], }, plugins: [ ... new HappyPack({ // 这个HappyPack的“名字”就叫作happyBabel,和楼上的查询参数遥相呼应 id: 'happyBabel', // 指定进程池 threadPool: happyThreadPool, loaders: ['babel-loader?cacheDirectory'] }) ], }
这样就能够并发处理多个任务
你们能够点进去看一下,它将建立一个交互式treemap可视化你的包的内容,这样你就能够知道哪些包是引发你打包体积过大的罪魁祸首
tree shaking 是一个术语,一般用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如
import
和export
。这个术语和概念其实是兴起于 ES2015 模块打包工具 rollup。新的 webpack 4 正式版本,扩展了这个检测能力,经过
package.json
的"sideEffects"
属性做为标记,向 compiler 提供提示,代表项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此能够安全地删除文件中未使用的部分。
若是全部代码都不包含反作用,咱们就能够简单地将该属性标记为 false
,来告知 webpack,它能够安全地删除未用到的 export 导出。
{ "name": "your-project", "sideEffects": false }
若是你的代码确实有一些反作用,那么能够改成提供一个数组:
数组方式支持相关文件的相对路径、绝对路径和 glob 模式。它在内部使用 micromatch。
注意,任何导入的文件都会受到 tree shaking 的影响。这意味着,若是在项目中使用相似 css-loader
并导入 CSS 文件,则须要将其添加到 side effect 列表中,以避免在生产模式中无心中将它删除:
{ "name": "your-project", "sideEffects": [ "./src/some-side-effectful-file.js" "*.css" ] }
Tree-Shaking 的针对性很强,它更适合用来处理模块级别的冗余代码
// 在压缩过程当中对碎片化的冗余代码(如 console 语句、注释等)进行自动化删除 const webpack = require('webpack'); module.exports = { plugins: [ new webpack.optimize.UglifyJsPlugin({ // 容许并发 parallel: true, // 开启缓存 cache: true, compress: { // 删除全部的console语句 drop_console: true, // 把使用屡次的静态值自动定义为变量 reduce_vars: true, }, output: { // 不保留注释 comment: false, // 使输出的代码尽量紧凑 beautify: false } }), ] }
好比路由的懒加载,tab组件的懒加载
gzip的基础是DEFLATE,DEFLATE是LZ77与哈夫曼编码的一个组合体。DEFLATE最初是做为LZW以及其它受专利保护的数据压缩算法的替代版本而设计的,当时那些专利限制了compress以及其它一些流行的归档工具的应用
开启gzip压缩,只须要在请求头中加上
accept-encoding:gzip
压缩后一般能帮咱们减小响应 70% 左右的大小
Gzip 压缩背后的原理,是在一个文本文件中找出一些重复出现的字符串、临时替换它们,从而使整个文件变小。根据这个原理,文件中代码的重复率越高,那么压缩的效率就越高,使用 Gzip 的收益也就越大。反之亦然
图片优化就是在图片体积和图片质量之间作权衡
前置知识: 在计算机中,像素用二进制数来表示, n位二进制能够表示2^n种颜色,
因此二进制位数越多,可表示的颜色种类就越多,成像效果越细腻,图片体积越大
咱们须要在不一样的业务场景选择合适的图片类型
体积小有损压缩不支持透明加载快
当咱们把图片体积压缩至原有体积的 50% 如下时,JPG 仍然能够保持住 60% 的品质。此外,JPG 格式以 24 位存储单个图,能够呈现多达2^24 = 16,777,216 种颜色,足以应对大多数场景下对色彩的要求
使用场景
JPG 图片常常做为大的背景图、轮播图或 Banner 图出现,使用 JPG 呈现大图,既能够保住图片的质量,又不会带来使人头疼的图片体积,是当下比较推崇的一种方案。
体积大无损压缩支持透明质量高
8和24表明2进制位数
Png-8: 2^8 = 256中颜色
Png-24: 2^24 = 16,777,216 种颜色
追求极致的显示效果,不在乎图片大小的能够选择png-24
实践中,若是png-8没有带来视觉可辨别的色彩缺陷,考虑到体积,通常使用png-8
使用场景
考虑到 PNG 在处理线条和颜色对比度方面的优点,咱们主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等。
性能方面堪称业界楷模的淘宝首页页面上的 Logo,不管大小,都是 PNG 格式
体积小不失真文本文件兼容性好
SVG(可缩放矢量图形)是一种基于 XML 语法的图像格式。它和本文说起的其它图片种类有着本质的不一样:SVG 对图像的处理不是基于像素点,而是是基于对图像的形状描述。
优点
劣势
使用场景
小图标
文本文件依赖编码小图标解决方案
Base64 并不是一种图片格式,而是一种编码方式。Base64 和雪碧图同样,是做为小图标解决方案而存在的。在了解 Base64 以前,咱们先来了解一下雪碧图
雪碧图
图像精灵(sprite,意为精灵),被运用于众多使用大量小图标的网页应用之上。它可取图像的一部分来使用,使得使用一个图像文件替代多个小文件成为可能。相较于一个小图标一个图像文件,单独一张图片所需的 HTTP 请求更少,对内存和带宽更加友好。
前端使用background-position 来获取不一样位置的图标
Base64 图片的出现,也是为了减小加载网页图片时对服务器的请求次数,从而提高网页性能。Base64 是做为雪碧图的补充而存在的。
使用场景
小logo, 小icon
劣势
Base64 编码后,图片大小会膨胀为原文件的 4/3(这是由 Base64 的编码原理决定的),
若是咱们把大图也编码到 HTML 或 CSS 文件中,后者的体积会明显增长,即使咱们减小了 HTTP 请求,也没法弥补这庞大的体积带来的性能开销,得不偿失
base64编码工具推荐
webpack 的 url-loader , limit参数表示 指定文件的最大大小,以字节为单位,只有8192byte一下大小的图片才会进行base64编码
module.exports = { module: { rules: [ { test: /\.(png|jpg|gif)$/i, use: [ { loader: 'url-loader', options: { limit: 8192, }, }, ], }, ], }, };
年轻的全能型选手
它于 2010 年被提出, 是 Google 专为 Web 开发的一种旨在加快图片加载速度的图片格式,它支持有损压缩和无损压缩
与 PNG 相比,WebP 无损图像的尺寸缩小了 26%。在等效的 SSIM 质量指数下,WebP 有损图像比同类 JPEG 图像小 25-34%。
应用场景
要使用webP咱们就必须为不兼容的浏览器进行降级处理
咱们能够看看淘宝是怎么作的
在谷歌浏览器打开淘宝,打开控制台,搜索.webp
其中一个img的src是这样的
img.alicdn.com/imgextra/i3/6000000002336/O1CN019A9rll1T7vqVs4Wur_!!6000000002336-0-octopus.jpg_400x400q90.jpg_.webp
在safari中打开淘宝,打开控制台,查看同一张图片的src
img.alicdn.com/imgextra/i3/6000000002336/O1CN019A9rll1T7vqVs4Wur_!!6000000002336-0-octopus.jpg_400x400q90.jpg
淘宝是会根据浏览器的型号来判断是否支持webp,不支持就把.webp后缀切换成.jpg
更灵活的方案
把判断逻辑交给后端,服务器根据 请求头的 Accept 字段 来判断是否支持webp, 不然返回原图(jpg),
这样作的好处是当webp的兼容性发生变化时,前端不用修改判断是否支持webp的代码
经过网络获取内容既速度缓慢又开销巨大。较大的响应须要在客户端与服务器之间进行屡次往返通讯,这会延迟浏览器得到和处理内容的时间,还会增长访问者的流量费用。所以,缓存并重复利用以前获取的资源的能力成为性能优化的一个关键方面。
按照获取资源时请求的优先级排序:
是指存在内存中的缓存。从优先级上来讲,它是浏览器最早尝试去命中的一种缓存。从效率上来讲,它是响应速度最快的一种缓存。
内存缓存是快的,也是“短命”的。它和渲染进程“生死相依”,当进程结束后,也就是 tab 关闭之后,内存里的数据也将不复存在
资源存不存内存,浏览器秉承的是“节约原则”。咱们发现,Base64 格式的图片,几乎永远能够被塞进 memory cache,这能够视做浏览器为节省渲染开销的“自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的概率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们每每被直接甩进磁盘
Service Worker 是一种独立于主线程以外的 Javascript 线程。它脱离于浏览器窗体,所以没法直接访问 DOM。这样独立的个性使得 Service Worker 的“我的行为”没法干扰页面的性能,这个“幕后工做者”能够帮咱们实现离线缓存、消息推送和网络代理等功能。咱们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。
Service Worker 的生命周期包括 install、active、working三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非咱们主动终止它。这是它能够用来实现离线存储的重要先决条件。
如何使用 service worker(必须以 https 协议为前提)
新建test.js文件
// Service Worker会监听 install事件,咱们在其对应的回调里能够实现初始化的逻辑 self.addEventListener('install', event => { event.waitUntil( // 考虑到缓存也须要更新,open内传入的参数为缓存的版本号 // caches 为浏览器的API // CacheStorage {} // __proto__: CacheStorage // delete: ƒ delete() // has: ƒ has() // keys: ƒ keys() // match: ƒ match() // open: ƒ open() // constructor: ƒ CacheStorage() // Symbol(Symbol.toStringTag): "CacheStorage" // __proto__: Object caches.open('test-v1').then(cache => { return cache.addAll([ // 此处传入指定的需缓存的文件名 '/test.html', '/test.css', '/test.js' ]) }) ) }) // Service Worker会监听全部的网络请求,网络请求的产生触发的是fetch事件,咱们能够在其对应的监听函数中实现对请求的拦截,进而判断是否有对应到该请求的缓存,实现从Service Worker中取到缓存的目的 self.addEventListener('fetch', event => { event.respondWith( // 尝试匹配该请求对应的缓存值 caches.match(event.request).then(res => { // 若是匹配到了,调用Server Worker缓存 if (res) { return res; } // 若是没匹配到,向服务端发起这个资源请求 return fetch(event.request).then(response => { if (!response || response.status !== 200) { return response; } // 请求成功的话,将请求缓存起来。 caches.open('test-v1').then(function(cache) { cache.put(event.request, response); }); return response.clone(); }); }) ); });
在项目代码入口j s文件中加入
window.navigator.serviceWorker.register('/test.js').then( function () { console.log('注册成功') }).catch(err => { console.error("注册失败") })
HTTP 缓存是咱们平常开发中最为熟悉的一种缓存机制。它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的状况下,才会走协商缓存。
强缓存是利用 http 头中的 Expires 和 Cache-Control两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通讯。
命中强缓存的状况下,返回的 HTTP 状态码为200 (from disk cache)
Cache-Control 的 max-age 配置项相对于 expires 的优先级更高。当 Cache-Control 与 expires 同时出现时,咱们以 Cache-Control 为准。
cache-control: max-age=31536000
max-age表示的有效时长, 单位是秒,表示该资源31536000秒内是有效的
public 与 private
public 与 private 是针对资源是否可以被代理服务缓存而存在的一组对立概念。
若是咱们为资源设置了 public,那么它既能够被浏览器缓存,也能够被代理服务器缓存;若是咱们设置了 private,则该资源只能被浏览器缓存。private 为默认值。但多数状况下,public 并不须要咱们手动设置
no-store与no-cache
no-cache 绕开了浏览器:咱们为资源设置了 no-cache 后,每一次发起请求都不会再去询问浏览器的缓存状况,而是直接向服务端去确认该资源是否过时(不走强缓存)。
no-store 比较绝情,顾名思义就是不使用任何缓存策略。在 no-cache 的基础上,它连服务端的缓存确认也绕开了,只容许你直接向服务端发送请求、并下载完整的响应。
expires: Wed, 11 Sep 2019 16:12:18 GMT
expires 设置的是资源到期时间(服务器时间)
接下来若是咱们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,若是本地时间小于 expires 设定的过时时间,那么就直接去缓存中取这个资源。因为时间戳是服务器来定义的,而本地时间的取值却来自客户端,所以 expires 的工做机制对客户端时间与服务器时间之间的一致性提出了极高的要求,若服务器与客户端存在时差,将带来意料以外的结果。
协商缓存机制下,浏览器须要向服务器去询问缓存的相关信息,进而判断是从新发起请求、下载完整的响应,仍是从本地获取缓存的资源。
若是服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种状况下网络请求对应的状态码是 304
Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准.
首次请求时,响应头里会携带Last-Modified字段,像这样
Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT
再次请求相同的资源,请求头里会带上If-Modified-Since字段,
值是上一次请求响应头的Last-Modified的值
If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT
服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。若是发生了变化,就会返回一个完整的响应内容,并在 响应头 中添加新的 Last-Modified 值;不然,返回如上图的 304 响应,响应头 不会再添加 Last-Modified 字段。
弊端
- 咱们编辑了文件,但文件的内容没有改变。服务端并不清楚咱们是否真正改变了文件,它仍然经过最后编辑时间进行判断。所以这个资源在再次被请求时,会被当作新资源,进而引起一次完整的响应——不应从新请求的时候,也会从新请求
- 当咱们修改文件的速度过快时(好比花了 100ms 完成了改动),因为 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,因此它是感知不到这个改动的——该从新请求的时候,反而没有从新请求了
为了解决这样的问题,Etag 做为 Last-Modified 的补充出现了
Etag 是由服务器为每一个资源生成的惟一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不一样,它们对应的 Etag 就是不一样的,反之亦然。所以 Etag 可以精准地感知文件的变化。
首次请求时,响应头里会携带ETag字段,像这样
ETag: W/"2a3b-1602480f459"
再次请求相同的资源,请求头里会带上If-Modified-Since字段,
值是上一次请求响应头的ETag的值
If-None-Match: W/"2a3b-1602480f459"
服务器会拿If-None-Match的值和服务器资源的当前标识对比,相同返回304, 不一样返回新的资源并带上新的Etag
Etag 的生成过程须要服务器额外付出开销,会影响服务端的性能,这是它的弊端。所以启用 Etag 须要咱们审时度势。正如咱们刚刚所提到的——Etag 并不能替代 Last-Modified,它只能做为 Last-Modified 的补充和强化存在。
Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。
- Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、 Service Worker Cache和 HTTP Cache 均未命中的状况下才会去询问 Push Cache。
- Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
- 不一样的页面只要共享了同一个 HTTP2 链接,那么它们就能够共享同一个 Push Cache。
Cookie 的本职工做并不是本地存储,而是“维持状态”。
在 Web 开发的早期,人们亟需解决的一个问题就是状态管理的问题:HTTP 协议是一个无状态协议,服务器接收客户端的请求,返回一个响应,故事到此就结束了,服务器并无记录下关于客户端的任何信息。那么下次请求的时候,如何让服务器知道“我是我”呢?
在这样的背景下,Cookie 应运而生。
同一个域名下的全部请求,都会携带 Cookie。
Cookie 以键值对形式存储
服务器经过响应头的set-cookie: 字段来设置客户端cookie
客户端会在请求头的Cookie字段里携带cookie
客户端能够经过document.cookie来获取/设置cookie
Cookie 是有体积上限的,它最大只能有 4KB。
当 Cookie 超过 4KB 时,它将面临被裁切的命运。这样看来,Cookie 只能用来存取少许的信息。
为了弥补 Cookie 的局限性,让“专业的人作专业的事情”,Web Storage 出现了。
Web Storage 是 HTML5 专门为浏览器存储而提供的数据存储机制。它又分为 Local Storage 与 Session Storage。
Local Storage 与 Session Storage 的区别
二者的区别在于生命周期与做用域的不一样。
Web Storage 的特性
使用场景
IndexedDB 是一个运行在浏览器上的非关系型数据库
理论上来讲,IndexedDB 是没有存储上限的(通常来讲不会小于 250M)。它不只能够存储字符串,还能够存储二进制数据。
下面来熟悉一下它的使用流程
建立/打开一个数据库
if (!window.indexedDB) { window.alert("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.") } // 后面的回调中,咱们能够经过event.target.result拿到数据库实例 let db // open 第一个参数为数据库名,第二个参数为版本号 // 没有就建立,有就打开 const request = window.indexedDB.open("xiaoceDB", 1) // 使用IndexedDB失败时的监听函数 request.onerror = function(event) { console.log('没法使用IndexedDB') } // 成功 request.onsuccess = function(event){ // 此处就能够获取到db实例 db = event.target.result console.log("你打开了IndexedDB") }
建表(表对应indexDB数据库的store)
// onupgradeneeded事件会在初始化数据库/版本发生更新时被调用, // 咱们在它的监听函数中建立object store request.onupgradeneeded = function(event){ let objectStore // 若是同名表未被建立过,则新建test表 if (!db.objectStoreNames.contains('test')) { objectStore = db.createObjectStore('test', { keyPath: 'id' }) } }
构建一个事务来执行一些数据库操做,像增长或提取数据等。
// 建立事务,指定表格名称和读写权限 const transaction = db.transaction(["test"],"readwrite") transaction.oncomplete = function(event) { // 在全部数据添加完毕后的处理 }; transaction.onerror = function(event) { // 不要忘记错误处理! }; // 拿到Object Store对象 const objectStore = transaction.objectStore("test") // 向表格写入数据 const request = objectStore.add({id: 1, name: 'xiuyan'}) request.onsuccess = function(event) { // 插入成功 };
使用场景
经过上面的示例你们能够看出,在 IndexedDB 中,咱们能够建立多个数据库,一个数据库中建立多张表,一张表中存储多条数据——这足以 hold 住复杂的结构性数据。IndexedDB 能够看作是 LocalStorage 的一个升级,当数据的复杂度和规模上升到了 LocalStorage 没法解决的程度,咱们毫无疑问能够请出 IndexedDB 来帮忙。
CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,所以服务器能够根据哪些服务器与用户距离最近,来知足数据的请求。 CDN 提供快速服务,较少受高流量影响。
本地存储带来的性能提高,只能在“获取到资源并把它们存起来”这件事情发生,也就是说,首次请求资源的时候,这些招数都是救不了咱们的。要提高首次请求的响应能力,咱们还须要借助 CDN 的能力。
CDN 的核心点有两个,一个是缓存,一个是回源。
“缓存”就是说咱们把资源 copy 一份到 CDN 服务器上这个过程
“回源”就是说 CDN 发现本身没有这个资源(通常是缓存的数据过时了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。
CDN工做原理
CDN 每每被用来存放静态资源。上文中咱们所提到的“根服务器”本质上是业务服务器,它的核心任务在于生成动态页面或返回非纯静态页面,这两种过程都是须要计算的。业务服务器仿佛一个车间,车间里运转的机器轰鸣着为咱们产出所需的资源;相比之下,CDN 服务器则像一个仓库,它只充当资源的“栖息地”和“搬运工”。
静态资源: 就是像 JS、CSS、图片等不须要业务服务器进行计算即得的资源。
动态资源: 顾名思义是须要后端实时动态生成的资源,较为常见的就是 JSP、ASP 或者依赖服务端渲染获得的 HTML 页面。
非纯静态资源: 它是指须要服务器在页面以外做额外计算的 HTML 页面。具体来讲,当我打开某一网站以前,该网站须要经过权限认证等一系列手段确认个人身份、进而决定是否要把 HTML 页面呈现给我。这种状况下 HTML 确实是静态的,但它和业务服务器的操做耦合,咱们把它丢到CDN 上显然是不合适的。
CDN 的实际应用
服务端渲染的模式下,当用户第一次请求页面时,由服务器把须要的组件或页面渲染成 HTML 字符串,而后把它返回给客户端。客户端拿到手的,是能够直接渲染而后呈现给用户的 HTML 内容,不须要为了生成 DOM 内容本身再去跑一遍 JS 代码。
使用服务端渲染的网站,能够说是“所见即所得”,页面上呈现的内容,咱们在 html 源文件里也能找到。
Tip: 客户端渲染: 页面上呈现的内容,你在 html 源文件里里找不到
解决的问题
应用场景
服务端渲染本质上是本该浏览器作的事情,分担给服务器去作。这样当资源抵达浏览器时,它呈现的速度就快了
可是这样会增长服务器的压力
在实践中,服务器稀少而宝贵,但首屏渲染体验和 SEO 的优化方案却不少——咱们最好先把能用的低成本“大招”都用完。
渲染引擎和JS引擎
随着 JS 引擎愈来愈独立,内核也成了渲染引擎的代称
渲染引擎又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件
常见浏览器内核
Trident(IE) Gecko(火狐) Blink(Chrome、Opera) Webkit(Safari)
Blink 是 webkit 衍生而来的一个分支
以 webkit 内核为例,剖析浏览器的渲染过程
初次渲染完成后,每当一个新元素加入到Dom树中,浏览器会经过c s s引擎查c s s样式表, 找到符合该元素的样式规则应用到这个元素上,而后再从新去绘制它.
在找元素样式这个过程咱们就能够进行代码层次的优化:
首先c s s引擎查找样式表的规则是,对每条样式从右到左的顺序去匹配
而并非按咱们的写法从左到右
因此这种写法耗时更长
#my li {}
c s s引擎先匹配全部li, 再去匹配li的父级是#my,
更坑的是使用通配符选择器重置样式
* {}
c s s引擎会匹配全部元素,耗费的时间更长。
.my_li {}
拒绝嵌套 拒绝嵌套 拒绝嵌套,重要的话说三遍
HTML、CSS 和 JS,都具备阻塞渲染的特性。
HTML 阻塞,天经地义——没有 HTML,何来 DOM?没有 DOM,渲染和优化,都是空谈。
前面已经说了,须要CSSOM和DOM一块儿构建Render Tree, 因此即使DOM解析完了,只要CSSOM没解析完,页面就不会渲染,因此
CSS 是阻塞渲染的资源。须要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。
也就有了
尽早: 把c s s 放在head标签里
尽快: 启用 CDN 实现静态资源加载速度的优化
正常模式引入j s文件 和内联j s代码都会阻塞dom树的解析,浏览器会暂时将控制权转交给JS引擎,j s执行完毕后, 浏览器会将控制权还给渲染引擎继续dom树的解析
外部引入js的加载时机是可控的
有三种模式可选
正常模式
<script src="index.js"></script>
这种状况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去作其它事情。
async 模式:
<script async src="index.js"></script>
async 模式下,JS 不会阻塞浏览器作任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会当即执行。
defer 模式:
<script defer src="index.js"></script>
defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。
从应用的角度来讲,通常当咱们的脚本与 DOM 元素和其它脚本之间的依赖关系不强时,咱们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,咱们会选用 defer。
为何dom慢
把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁链接。——《高性能 JavaScript》
当咱们用 JS 去操做 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口做为“桥梁”。
过“桥”要收费——这个开销自己就是不可忽略的。咱们每操做一次 DOM(无论是为了修改仍是仅仅为了访问其值 ),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。所以“减小 DOM 操做”的建议,并不是空穴来风。
当咱们对 DOM 的修改会引起它外观(样式)上的改变时,就会触发回流或重绘。
- 回流:当咱们对 DOM 的修改引起了 DOM 几何尺寸的变化(好比修改元素的宽、高或隐藏元素等)时,浏览器须要从新计算元素的几何属性(其余元素的几何属性和位置也会所以受到影响),而后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
- 重绘:当咱们对 DOM 的修改致使了样式的变化、却并未影响其几何属性(好比修改了颜色或背景色)时,浏览器不需从新计算元素的几何属性、直接为该元素绘制新的样式(跳过了回流环节)。这个过程叫作重绘。
由此咱们能够看出,重绘不必定致使回流,回流必定会致使重绘
咱们在开发中,要从代码层面出发,尽量把回流和重绘的次数最小化
触发回流的操做:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
谷歌浏览器的优化: 会把一段时间内的回流/重绘操做扔进一个flush队列里,不得已(上面的触发回流的操做)或者达到了必定的时间间隔再一次性出列
操做建议
浏览器中的事件循环:
前置知识, 异步队列:
macroTask: 宏任务队列,setTimeout、setInterval、 setImmediate、script(总体代码)、 I/O 操做、UI 渲染等
microTask: 微任务队列,process.nextTick、Promise、MutationObserver 等
宏任务
执行前,GUI渲染线程
开始工做,对页面进行渲染node中的事件循环
Node会先执行全部类型为 timers 的 MacroTask,而后执行全部的 MicroTask(NextTick例外) 进入 poll 阶段,执行几乎全部 MacroTask,而后执行全部的 MicroTask 再执行全部类型为 check 的 MacroTask,而后执行全部的 MicroTask 再执行全部类型为 close callbacks 的 MacroTask,而后执行全部的 MicroTask 至此,完成一个 Tick,回到 timers 阶段 …… 如此反复,无穷无尽……
// 节流, duration内不管触发多少次,只执行一次 // 使用一个变量做为节流的开关 const throttle = function (fn, duration = 300) { if (!fn || !(fn instanceof Function)) { throw('必须传一个函数做为参数') } let flag = true; return function(...args) { if (flag) { flag = false; console.log(1111) fn.apply(this, args) } else { return; } setTimeout( () => { flag = true; }, duration ) } } const throttleFn = throttle(() => console.log('触发了滚动事件')); document.addEventListener('scroll', throttleFn) // 使用时间戳来判断节流的时间 const throttle1 = function (fn, duration = 300) { if (!fn || !(fn instanceof Function)) { throw('必须传一个函数做为参数') } let now = null; return function(...args) { const cur = new Date().getTime(); if (cur - now > duration) { fn.apply(this, args) now = cur; } } }
// 防抖 事件中止触发后的 duraion后执行回调 const debounce = function (fn, duration = 300) { if (!fn || !(fn instanceof Function)) { throw('必须传一个函数做为参数') } let timeout = null; return function(...args) { if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { fn.apply(this, args) }, duration) } } document.addEventListener('scroll', debounce(() => console.log('触发了滚动事件')))
W3C 规范为咱们提供了 Performance 相关的接口。它容许咱们获取到用户访问一个页面的每一个阶段的精确时间,从而对性能进行分析。咱们能够将其理解为 Performance 面板的进一步细化与可编程化。
// 性能API window.performance // 控制台能够看到如下属性 eventCounts: EventCounts {size: 36} memory: MemoryInfo {totalJSHeapSize: 35927203, usedJSHeapSize: 33471483, jsHeapSizeLimit: 4294705152} navigation: PerformanceNavigation {type: 0, redirectCount: 0} onresourcetimingbufferfull: null timeOrigin: 1605666468542.5789 timing: PerformanceTiming connectEnd: 1605666468548 connectStart: 1605666468548 domComplete: 1605666468969 domContentLoadedEventEnd: 1605666468778 domContentLoadedEventStart: 1605666468778 domInteractive: 1605666468716 domLoading: 1605666468666 domainLookupEnd: 1605666468548 domainLookupStart: 1605666468548 fetchStart: 1605666468548 loadEventEnd: 1605666468969 loadEventStart: 1605666468969 navigationStart: 1605666468542 redirectEnd: 0 redirectStart: 0 requestStart: 1605666468563 responseEnd: 1605666468655 responseStart: 1605666468654 secureConnectionStart: 0 unloadEventEnd: 1605666468664 unloadEventStart: 1605666468663 __proto__: PerformanceTiming __proto__: Performance const timing = window.performance.timing // DNS查询耗时 timing.domainLookupEnd - timing.domainLookupStart // TCP链接耗时 timing.connectEnd - timing.connectStart // 内容加载耗时 timing.responseEnd - timing.requestStart // 关键性能指标 // firstbyte:首包时间 timing.responseStart – timing.domainLookupStart // fpt:First Paint Time, 首次渲染时间 / 白屏时间 timing.responseEnd – timing.fetchStart // tti:Time to Interact,首次可交互时间 timing.domInteractive – timing.fetchStart // ready:HTML 加载完成时间,即 DOM 就位的时间 timing.domContentLoaded – timing.fetchStart // load:页面彻底加载时间 timing.loadEventStart – timing.fetchStart