lllllxt_in_sf

lllllxt_in_sf 查看完整档案

珠海编辑  |  填写毕业院校远光软件  |  前端工程师 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

lllllxt_in_sf 赞了文章 · 3月29日

前端性能优化 24 条建议(2020)

性能优化是把双刃剑,有好的一面也有坏的一面。好的一面就是能提升网站性能,坏的一面就是配置麻烦,或者要遵守的规则太多。并且某些性能优化规则并不适用所有场景,需要谨慎使用,请读者带着批判性的眼光来阅读本文。

本文相关的优化建议的引用资料出处均会在建议后面给出,或者放在文末。

1. 减少 HTTP 请求

一个完整的 HTTP 请求需要经历 DNS 查找,TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。接下来看一个具体的例子帮助理解 HTTP :

在这里插入图片描述

这是一个 HTTP 请求,请求的文件大小为 28.4KB。

名词解释:

  • Queueing: 在请求队列中的时间。
  • Stalled: 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。
  • Proxy negotiation: 与代理服务器连接进行协商所花费的时间。
  • DNS Lookup: 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。
  • Initial Connection / Connecting: 建立连接所花费的时间,包括TCP握手/重试和协商SSL。
  • SSL: 完成SSL握手所花费的时间。
  • Request sent: 发出网络请求所花费的时间,通常为一毫秒的时间。
  • Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间。
  • Content Download: 接收响应数据所花费的时间。

从这个例子可以看出,真正下载数据的时间占比为 13.05 / 204.16 = 6.39%,文件越小,这个比例越小,文件越大,比例就越高。这就是为什么要建议将多个小文件合并为一个大文件,从而减少 HTTP 请求次数的原因。

参考资料:

2. 使用 HTTP2

HTTP2 相比 HTTP1.1 有如下几个优点:

解析速度快

服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段。

多路复用

HTTP1.1 如果要同时发起多个请求,就得建立多个 TCP 连接,因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。

在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。同一个请求和响应用一个流来表示,并有唯一的流 ID 来标识。
多个请求和响应在 TCP 连接中可以乱序发送,到达目的地后再通过流 ID 重新组建。

首部压缩

HTTP2 提供了首部压缩功能。

例如有如下两个请求:

:authority: unpkg.zhimg.com
:method: GET
:path: /za-js-sdk@2.16.0/dist/zap.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
:authority: zz.bdstatic.com
:method: GET
:path: /linksubmit/push.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36

从上面两个请求可以看出来,有很多数据都是重复的。如果可以把相同的首部存储起来,仅发送它们之间不同的部分,就可以节省不少的流量,加快请求的时间。

HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送。

下面再来看一个简化的例子,假设客户端按顺序发送如下请求首部:

Header1:foo
Header2:bar
Header3:bat

当客户端发送请求时,它会根据首部值创建一张表:

索引首部名称
62Header1foo
63Header2bar
64Header3bat

如果服务器收到了请求,它会照样创建一张表。
当客户端发送下一个请求的时候,如果首部相同,它可以直接发送这样的首部块:

62 63 64

服务器会查找先前建立的表格,并把这些数字还原成索引对应的完整首部。

优先级

HTTP2 可以对比较紧急的请求设置一个较高的优先级,服务器在收到这样的请求后,可以优先处理。

流量控制

由于一个 TCP 连接流量带宽(根据客户端到服务器的网络带宽而定)是固定的,当有多个请求并发时,一个请求占的流量多,另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制。

服务器推送

HTTP2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。

例如当浏览器请求一个网站时,除了返回 HTML 页面外,服务器还可以根据 HTML 页面中的资源的 URL,来提前推送资源。

现在有很多网站已经开始使用 HTTP2 了,例如知乎:

在这里插入图片描述

其中 h2 是指 HTTP2 协议,http/1.1 则是指 HTTP1.1 协议。

参考资料:

3. 使用服务端渲染

客户端渲染: 获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。

服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。

  • 优点:首屏渲染快,SEO 好。
  • 缺点:配置麻烦,增加了服务器的计算压力。

下面我用 Vue SSR 做示例,简单的描述一下 SSR 过程。

客户端渲染过程

  1. 访问客户端渲染的网站。
  2. 服务器返回一个包含了引入资源语句和 <div id="app"></div> 的 HTML 文件。
  3. 客户端通过 HTTP 向服务器请求资源,当必要的资源都加载完毕后,执行 new Vue() 开始实例化并渲染页面。

服务端渲染过程

  1. 访问服务端渲染的网站。
  2. 服务器会查看当前路由组件需要哪些资源文件,然后将这些文件的内容填充到 HTML 文件。如果有 ajax 请求,就会执行它进行数据预取并填充到 HTML 文件里,最后返回这个 HTML 页面。
  3. 当客户端接收到这个 HTML 页面时,可以马上就开始渲染页面。与此同时,页面也会加载资源,当必要的资源都加载完毕后,开始执行 new Vue() 开始实例化并接管页面。

从上述两个过程中可以看出,区别就在于第二步。客户端渲染的网站会直接返回 HTML 文件,而服务端渲染的网站则会渲染完页面再返回这个 HTML 文件。

这样做的好处是什么?是更快的内容到达时间 (time-to-content)

假设你的网站需要加载完 abcd 四个文件才能渲染完毕。并且每个文件大小为 1 M。

这样一算:客户端渲染的网站需要加载 4 个文件和 HTML 文件才能完成首页渲染,总计大小为 4M(忽略 HTML 文件大小)。而服务端渲染的网站只需要加载一个渲染完毕的 HTML 文件就能完成首页渲染,总计大小为已经渲染完毕的 HTML 文件(这种文件不会太大,一般为几百K,我的个人博客网站(SSR)加载的 HTML 文件为 400K)。这就是服务端渲染更快的原因

参考资料:

4. 静态资源使用 CDN

内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。

CDN 原理

当用户访问一个网站时,如果没有 CDN,过程是这样的:

  1. 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。
  2. 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到网站服务器的 IP 地址。
  3. 本地 DNS 将 IP 地址发回给浏览器,浏览器向网站服务器 IP 地址发出请求并得到资源。

如果用户访问的网站部署了 CDN,过程是这样的:

  1. 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。
  2. 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到全局负载均衡系统(GSLB)的 IP 地址。
  3. 本地 DNS 再向 GSLB 发出请求,GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户的位置,筛选出距离用户较近的本地负载均衡系统(SLB),并将该 SLB 的 IP 地址作为结果返回给本地 DNS。
  4. 本地 DNS 将 SLB 的 IP 地址发回给浏览器,浏览器向 SLB 发出请求。
  5. SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器发回给浏览器。
  6. 浏览器再根据 SLB 发回的地址重定向到缓存服务器。
  7. 如果缓存服务器有浏览器需要的资源,就将资源发回给浏览器。如果没有,就向源服务器请求资源,再发给浏览器并缓存在本地。

参考资料:

5. 将 CSS 放在文件头部,JavaScript 文件放在底部

所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。如果这些 CSS 和 JS 需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。

那为什么 CSS 文件还要放在头部呢?

因为先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。

另外,JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。

6. 使用字体图标 iconfont 代替图片图标

字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。

压缩字体文件

使用 fontmin-webpack 插件对字体文件进行压缩(感谢前端小伟提供)。

参考资料:

7. 善用缓存,不重复加载相同的资源

为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires 或 max-age 来控制这一行为。Expires 设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。而 max-age 是一个相对时间,建议使用 max-age 代替 Expires 。

不过这样会产生一个问题,当文件更新了怎么办?怎么通知浏览器重新请求文件?

可以通过更新页面中引用的资源链接地址,让浏览器主动放弃缓存,加载新资源。

具体做法是把资源地址 URL 的修改与文件内容关联起来,也就是说,只有文件内容变化,才会导致相应 URL 的变更,从而实现文件级别的精确缓存控制。什么东西与文件内容相关呢?我们会很自然的联想到利用数据摘要要算法对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。

参考资料:

8. 压缩文件

压缩文件可以减少文件下载时间,让用户体验性更好。

得益于 webpack 和 node 的发展,现在压缩文件已经非常方便了。

在 webpack 可以使用如下插件进行压缩:

  • JavaScript:UglifyPlugin
  • CSS :MiniCssExtractPlugin
  • HTML:HtmlWebpackPlugin

其实,我们还可以做得更好。那就是使用 gzip 压缩。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然,服务器也得支持这一功能。

gzip 是目前最流行和最有效的压缩方法。举个例子,我用 Vue 开发的项目构建后生成的 app.js 文件大小为 1.4MB,使用 gzip 压缩后只有 573KB,体积减少了将近 60%。

附上 webpack 和 node 配置 gzip 的使用方法。

下载插件

npm install compression-webpack-plugin --save-dev
npm install compression

webpack 配置

const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [new CompressionPlugin()],
}

node 配置

const compression = require('compression')
// 在其他中间件前使用
app.use(compression())

9. 图片优化

(1). 图片延迟加载

在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。

首先可以将图片这样设置,在页面不可见时图片不会加载:

<img data-data-original="https://avatars0.githubusercontent.com/u/22117876?s=460&u=7bd8f32788df6988833da6bd155c3cfbebc68006&v=4">

等页面可见时,使用 JS 加载图片:

const img = document.querySelector('img')
img.src = img.dataset.src

这样图片就加载出来了,完整的代码可以看一下参考资料。

参考资料:

(2). 响应式图片

响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。

通过 picture 实现

<picture>
    <source srcset="banner_w1000.jpg" media="(min-width: 801px)">
    <source srcset="banner_w800.jpg" media="(max-width: 800px)">
    <img data-original="banner_w800.jpg" alt="">
</picture>

通过 @media 实现

@media (min-width: 769px) {
    .bg {
        background-image: url(bg1080.jpg);
    }
}
@media (max-width: 768px) {
    .bg {
        background-image: url(bg768.jpg);
    }
}

(3). 调整图片大小

例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。

所以,我们可以用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。

(4). 降低图片质量

例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。我经常用 PS 切背景图时, 将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。

压缩方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。

以下附上 webpack 插件 image-webpack-loader 的用法。

npm i -D image-webpack-loader

webpack 配置

{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use:[
    {
    loader: 'url-loader',
    options: {
      limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    },
    /*对图片进行压缩*/
    {
      loader: 'image-webpack-loader',
      options: {
        bypassOnDebug: true,
      }
    }
  ]
}

(5). 尽可能利用 CSS3 效果代替图片

有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。

参考资料:

(6). 使用 webp 格式的图片

WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。

参考资料:

10. 通过 webpack 按需加载代码,提取第三库代码,减少 ES6 转为 ES5 的冗余代码

懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。

根据文件内容生成文件名,结合 import 动态引入组件实现按需加载

通过配置 output 的 filename 属性可以实现这个需求。filename 属性的值选项中有一个 [contenthash],它将根据文件内容创建出唯一 hash。当文件内容发生变化时,[contenthash] 也会发生变化。

output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js',
    path: path.resolve(__dirname, '../dist'),
},

提取第三方库

由于引入的第三方库一般都比较稳定,不会经常改变。所以将它们单独提取出来,作为长期缓存是一个更好的选择。
这里需要使用 webpack4 的 splitChunk 插件 cacheGroups 选项。

optimization: {
      runtimeChunk: {
        name: 'manifest' // 将 webpack 的 runtime 代码拆分为一个单独的 chunk。
    },
    splitChunks: {
        cacheGroups: {
            vendor: {
                name: 'chunk-vendors',
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                chunks: 'initial'
            },
            common: {
                name: 'chunk-common',
                minChunks: 2,
                priority: -20,
                chunks: 'initial',
                reuseExistingChunk: true
            }
        },
    }
},
  • test: 用于控制哪些模块被这个缓存组匹配到。原封不动传递出去的话,它默认会选择所有的模块。可以传递的值类型:RegExp、String和Function;
  • priority:表示抽取权重,数字越大表示优先级越高。因为一个 module 可能会满足多个 cacheGroups 的条件,那么抽取到哪个就由权重最高的说了算;
  • reuseExistingChunk:表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。
  • minChunks(默认是1):在分割之前,这个代码块最小应该被引用的次数(译注:保证代码块复用性,默认配置的策略是不需要多次引用也可以被分割)
  • chunks (默认是async) :initial、async和all
  • name(打包的chunks的名字):字符串或者函数(函数可以根据条件自定义名字)

减少 ES6 转为 ES5 的冗余代码

Babel 转化后的代码想要实现和原来代码一样的功能需要借助一些帮助函数,比如:

class Person {}

会被转换为:

"use strict";

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Person = function Person() {
  _classCallCheck(this, Person);
};

这里 _classCallCheck 就是一个 helper 函数,如果在很多文件里都声明了类,那么就会产生很多个这样的 helper 函数。

这里的 @babel/runtime 包就声明了所有需要用到的帮助函数,而 @babel/plugin-transform-runtime 的作用就是将所有需要 helper 函数的文件,从 @babel/runtime包 引进来:

"use strict";

var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck");

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}

var Person = function Person() {
  (0, _classCallCheck3.default)(this, Person);
};

这里就没有再编译出 helper 函数 classCallCheck 了,而是直接引用了 @babel/runtime 中的 helpers/classCallCheck

安装

npm i -D @babel/plugin-transform-runtime @babel/runtime

使用
.babelrc 文件中

"plugins": [
        "@babel/plugin-transform-runtime"
]

参考资料:

11. 减少重绘重排

浏览器渲染过程

  1. 解析HTML生成DOM树。
  2. 解析CSS生成CSSOM规则树。
  3. 将DOM树与CSSOM规则树合并在一起生成渲染树。
  4. 遍历渲染树开始布局,计算每个节点的位置大小信息。
  5. 将渲染树每个节点绘制到屏幕。

在这里插入图片描述

重排

当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。

重绘

当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致重排,例如改变字体颜色,只会导致重绘。记住,重排会导致重绘,重绘不会导致重排 。

重排和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。

什么操作会导致重排?

  • 添加或删除可见的 DOM 元素
  • 元素位置改变
  • 元素尺寸改变
  • 内容改变
  • 浏览器窗口尺寸改变

如何减少重排重绘?

  • 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
  • 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。

12. 使用事件委托

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。

<ul>
  <li>苹果</li>
  <li>香蕉</li>
  <li>凤梨</li>
</ul>

// good
document.querySelector('ul').onclick = (event) => {
  const target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

// bad
document.querySelectorAll('li').forEach((e) => {
  e.onclick = function() {
    console.log(this.innerHTML)
  }
}) 

13. 注意程序的局部性

一个编写良好的计算机程序常常具有良好的局部性,它们倾向于引用最近引用过的数据项附近的数据项,或者最近引用过的数据项本身,这种倾向性,被称为局部性原理。有良好局部性的程序比局部性差的程序运行得更快。

局部性通常有两种不同的形式:

  • 时间局部性:在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来被多次引用。
  • 空间局部性 :在一个具有良好空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。

时间局部性示例

function sum(arry) {
    let i, sum = 0
    let len = arry.length

    for (i = 0; i < len; i++) {
        sum += arry[i]
    }

    return sum
}

在这个例子中,变量sum在每次循环迭代中被引用一次,因此,对于sum来说,具有良好的时间局部性

空间局部性示例

具有良好空间局部性的程序

// 二维数组 
function sum1(arry, rows, cols) {
    let i, j, sum = 0

    for (i = 0; i < rows; i++) {
        for (j = 0; j < cols; j++) {
            sum += arry[i][j]
        }
    }
    return sum
}

空间局部性差的程序

// 二维数组 
function sum2(arry, rows, cols) {
    let i, j, sum = 0

    for (j = 0; j < cols; j++) {
        for (i = 0; i < rows; i++) {
            sum += arry[i][j]
        }
    }
    return sum
}

看一下上面的两个空间局部性示例,像示例中从每行开始按顺序访问数组每个元素的方式,称为具有步长为1的引用模式。
如果在数组中,每隔k个元素进行访问,就称为步长为k的引用模式。
一般而言,随着步长的增加,空间局部性下降。

这两个例子有什么区别?区别在于第一个示例是按行扫描数组,每扫描完一行再去扫下一行;第二个示例是按列来扫描数组,扫完一行中的一个元素,马上就去扫下一行中的同一列元素。

数组在内存中是按照行顺序来存放的,结果就是逐行扫描数组的示例得到了步长为 1 引用模式,具有良好的空间局部性;而另一个示例步长为 rows,空间局部性极差。

性能测试

运行环境:

  • cpu: i5-7400
  • 浏览器: chrome 70.0.3538.110

对一个长度为9000的二维数组(子数组长度也为9000)进行10次空间局部性测试,时间(毫秒)取平均值,结果如下:

所用示例为上述两个空间局部性示例

步长为 1步长为 9000
1242316

从以上测试结果来看,步长为 1 的数组执行时间比步长为 9000 的数组快了一个数量级。

总结:

  • 重复引用相同变量的程序具有良好的时间局部性
  • 对于具有步长为 k 的引用模式的程序,步长越小,空间局部性越好;而在内存中以大步长跳来跳去的程序空间局部性会很差

参考资料:

14. if-else 对比 switch

当判断条件数量越来越多时,越倾向于使用 switch 而不是 if-else。

if (color == 'blue') {

} else if (color == 'yellow') {

} else if (color == 'white') {

} else if (color == 'black') {

} else if (color == 'green') {

} else if (color == 'orange') {

} else if (color == 'pink') {

}

switch (color) {
    case 'blue':

        break
    case 'yellow':

        break
    case 'white':

        break
    case 'black':

        break
    case 'green':

        break
    case 'orange':

        break
    case 'pink':

        break
}

像以上这种情况,使用 switch 是最好的。假设 color 的值为 pink,则 if-else 语句要进行 7 次判断,switch 只需要进行一次判断。 从可读性来说,switch 语句也更好。

从使用时机来说,当条件值大于两个的时候,使用 switch 更好。不过 if-else 也有 switch 无法做到的事情,例如有多个判断条件的情况下,无法使用 switch。

15. 查找表

当条件语句特别多时,使用 switch 和 if-else 不是最佳的选择,这时不妨试一下查找表。查找表可以使用数组和对象来构建。

switch (index) {
    case '0':
        return result0
    case '1':
        return result1
    case '2':
        return result2
    case '3':
        return result3
    case '4':
        return result4
    case '5':
        return result5
    case '6':
        return result6
    case '7':
        return result7
    case '8':
        return result8
    case '9':
        return result9
    case '10':
        return result10
    case '11':
        return result11
}

可以将这个 switch 语句转换为查找表

const results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11]

return results[index]

如果条件语句不是数值而是字符串,可以用对象来建立查找表

const map = {
  red: result0,
  green: result1,
}

return map[color]

16. 避免页面卡顿

60fps 与设备刷新率

目前大多数设备的屏幕刷新率为 60 次/秒。因此,如果在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。
其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工作要做,因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。

在这里插入图片描述

假如你用 JavaScript 修改了 DOM,并触发样式修改,经历重排重绘最后画到屏幕上。如果这其中任意一项的执行时间过长,都会导致渲染这一帧的时间过长,平均帧率就会下降。假设这一帧花了 50 ms,那么此时的帧率为 1s / 50ms = 20fps,页面看起来就像卡顿了一样。

对于一些长时间运行的 JavaScript,我们可以使用定时器进行切分,延迟执行。

for (let i = 0, len = arry.length; i < len; i++) {
    process(arry[i])
}

假设上面的循环结构由于 process() 复杂度过高或数组元素太多,甚至两者都有,可以尝试一下切分。

const todo = arry.concat()
setTimeout(function() {
    process(todo.shift())
    if (todo.length) {
        setTimeout(arguments.callee, 25)
    } else {
        callback(arry)
    }
}, 25)

如果有兴趣了解更多,可以查看一下高性能JavaScript第 6 章和高效前端:Web高效编程与优化实践第 3 章。

参考资料:

17. 使用 requestAnimationFrame 来实现视觉变化

从第 16 点我们可以知道,大多数设备屏幕刷新率为 60 次/秒,也就是说每一帧的平均时间为 16.66 毫秒。在使用 JavaScript 实现动画效果的时候,最好的情况就是每次代码都是在帧的开头开始执行。而保证 JavaScript 在帧开始时运行的唯一方式是使用 requestAnimationFrame

/**
 * If run as a requestAnimationFrame callback, this
 * will be run at the start of the frame.
 */
function updateScreen(time) {
  // Make visual updates here.
}

requestAnimationFrame(updateScreen);

如果采取 setTimeoutsetInterval 来实现动画的话,回调函数将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿。

在这里插入图片描述

参考资料:

18. 使用 Web Workers

Web Worker 使用其他工作线程从而独立于主线程之外,它可以执行任务而不干扰用户界面。一个 worker 可以将消息发送到创建它的 JavaScript 代码, 通过将消息发送到该代码指定的事件处理程序(反之亦然)。

Web Worker 适用于那些处理纯数据,或者与浏览器 UI 无关的长时间运行脚本。

创建一个新的 worker 很简单,指定一个脚本的 URI 来执行 worker 线程(main.js):

var myWorker = new Worker('worker.js');
// 你可以通过postMessage() 方法和onmessage事件向worker发送消息。
first.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

second.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

在 worker 中接收到消息后,我们可以写一个事件处理函数代码作为响应(worker.js):

onmessage = function(e) {
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
  console.log('Posting message back to main script');
  postMessage(workerResult);
}

onmessage处理函数在接收到消息后马上执行,代码中消息本身作为事件的data属性进行使用。这里我们简单的对这2个数字作乘法处理并再次使用postMessage()方法,将结果回传给主线程。

回到主线程,我们再次使用onmessage以响应worker回传的消息:

myWorker.onmessage = function(e) {
  result.textContent = e.data;
  console.log('Message received from worker');
}

在这里我们获取消息事件的data,并且将它设置为result的textContent,所以用户可以直接看到运算的结果。

不过在worker内,不能直接操作DOM节点,也不能使用window对象的默认方法和属性。然而你可以使用大量window对象之下的东西,包括WebSockets,IndexedDB以及FireFox OS专用的Data Store API等数据存储机制。

参考资料:

19. 使用位操作

JavaScript 中的数字都使用 IEEE-754 标准以 64 位格式存储。但是在位操作中,数字被转换为有符号的 32 位格式。即使需要转换,位操作也比其他数学运算和布尔操作快得多。

取模

由于偶数的最低位为 0,奇数为 1,所以取模运算可以用位操作来代替。

if (value % 2) {
    // 奇数
} else {
    // 偶数 
}
// 位操作
if (value & 1) {
    // 奇数
} else {
    // 偶数
}
取整
~~10.12 // 10
~~10 // 10
~~'1.5' // 1
~~undefined // 0
~~null // 0
位掩码
const a = 1
const b = 2
const c = 4
const options = a | b | c

通过定义这些选项,可以用按位与操作来判断 a/b/c 是否在 options 中。

// 选项 b 是否在选项中
if (b & options) {
    ...
}

20. 不要覆盖原生方法

无论你的 JavaScript 代码如何优化,都比不上原生方法。因为原生方法是用低级语言写的(C/C++),并且被编译成机器码,成为浏览器的一部分。当原生方法可用时,尽量使用它们,特别是数学运算和 DOM 操作。

21. 降低 CSS 选择器的复杂性

(1). 浏览器读取选择器,遵循的原则是从选择器的右边到左边读取。

看个示例

#block .text p {
    color: red;
}
  1. 查找所有 P 元素。
  2. 查找结果 1 中的元素是否有类名为 text 的父元素
  3. 查找结果 2 中的元素是否有 id 为 block 的父元素

(2). CSS 选择器优先级

内联 > ID选择器 > 类选择器 > 标签选择器

根据以上两个信息可以得出结论。

  1. 选择器越短越好。
  2. 尽量使用高优先级的选择器,例如 ID 和类选择器。
  3. 避免使用通配符 *。

最后要说一句,据我查找的资料所得,CSS 选择器没有优化的必要,因为最慢和慢快的选择器性能差别非常小。

参考资料:

22. 使用 flexbox 而不是较早的布局模型

在早期的 CSS 布局方式中我们能对元素实行绝对定位、相对定位或浮动定位。而现在,我们有了新的布局方式 flexbox,它比起早期的布局方式来说有个优势,那就是性能比较好。

下面的截图显示了在 1300 个框上使用浮动的布局开销:

在这里插入图片描述

然后我们用 flexbox 来重现这个例子:

在这里插入图片描述

现在,对于相同数量的元素和相同的视觉外观,布局的时间要少得多(本例中为分别 3.5 毫秒和 14 毫秒)。

不过 flexbox 兼容性还是有点问题,不是所有浏览器都支持它,所以要谨慎使用。

各浏览器兼容性:

  • Chrome 29+
  • Firefox 28+
  • Internet Explorer 11
  • Opera 17+
  • Safari 6.1+ (prefixed with -webkit-)
  • Android 4.4+
  • iOS 7.1+ (prefixed with -webkit-)

参考资料:

23. 使用 transform 和 opacity 属性更改来实现动画

在 CSS 中,transforms 和 opacity 这两个属性更改不会触发重排与重绘,它们是可以由合成器(composite)单独处理的属性。

在这里插入图片描述

参考资料:

24. 合理使用规则,避免过度优化

性能优化主要分为两类:

  1. 加载时优化
  2. 运行时优化

上述 23 条建议中,属于加载时优化的是前面 10 条建议,属于运行时优化的是后面 13 条建议。通常来说,没有必要 23 条性能优化规则都用上,根据网站用户群体来做针对性的调整是最好的,节省精力,节省时间。

在解决问题之前,得先找出问题,否则无从下手。所以在做性能优化之前,最好先调查一下网站的加载性能和运行性能。

检查加载性能

一个网站加载性能如何主要看白屏时间和首屏时间。

  • 白屏时间:指从输入网址,到页面开始显示内容的时间。
  • 首屏时间:指从输入网址,到页面完全渲染的时间。

将以下脚本放在 </head> 前面就能获取白屏时间。

<script>
    new Date() - performance.timing.navigationStart
</script>

window.onload 事件里执行 new Date() - performance.timing.navigationStart 即可获取首屏时间。

检查运行性能

配合 chrome 的开发者工具,我们可以查看网站在运行时的性能。

打开网站,按 F12 选择 performance,点击左上角的灰色圆点,变成红色就代表开始记录了。这时可以模仿用户使用网站,在使用完毕后,点击 stop,然后你就能看到网站运行期间的性能报告。如果有红色的块,代表有掉帧的情况;如果是绿色,则代表 FPS 很好。performance 的具体使用方法请用搜索引擎搜索一下,毕竟篇幅有限。

通过检查加载和运行性能,相信你对网站性能已经有了大概了解。所以这时候要做的事情,就是使用上述 23 条建议尽情地去优化你的网站,加油!

参考资料:

其他参考资料

更多文章,欢迎关注

查看原文

赞 252 收藏 173 评论 10

lllllxt_in_sf 赞了文章 · 3月24日

一文读懂Http Headers为何物(超详细)

一、HTTP 请求内容

由于最新的http2,并没有被各大浏览器广泛使用,所以本文是基于http/1.1所编写的。
同时经过检测我们也发现,chrome等浏览器也正是使用http/1.1版本的。

图片描述
关于http/1.1协议的详情,可查看官方文档

我们打开chrome的network,点击任何一条request请求,即可发现,每个http headers都包含以下部分:Genaral,Request Headers,Response Headers,Request Payload。

General(不属于headers,只用于收集请求url和响应的status等信息)
clipboard.png

Request Headers(请求headers)
clipboard.png

Response Headers(响应headers)
clipboard.png

Request Payload(请求参数)
clipboard.png


二、HTTP Headers分类

在http heanders中,为了方便,分为以下几类:Genaral headers(和上面说的General不同,这个只是为了方便统计),Request Headers,Response Headers,Entity Headers(也是为了方便统计)。

图片描述

所以,一个完整的请求头/响应头,应该除了自身,还包括 General Headers和Entity Headers。
clipboard.png

1、Genaral headers: 同时适用于请求和响应消息,但与最终消息传输的数据无关的消息头。
2、Request Headers: 包含更多有关要获取的资源或客户端本身信息的消息头。
3、Response Headers:包含有关响应的补充信息,如其位置或服务器本身(名称和版本等)的消息头。
4、Entity Headers:包含有关实体主体的更多信息,比如主体长(Content-Length)度或其MIME类型。

1、Genaral headers

Cache-Control——控制缓存的行为; 详情
Connection——决定当前的事务完成后,是否会关闭网络连接; 详情
Date——创建报文的日期时间; 详情
Keep-Alive——用来设置超时时长和最大请求数;详情
Via——代理服务器的相关信息;详情
Warning——错误通知;详情
Trailer——允许发送方在分块发送的消息后面添加额外的元信息; 详情
Transfer-Encoding——指定报文主体的传输编码方式;详情
Upgrade——升级为其他协议;

2、Request headers

Accept——客户端可以处理的内容类型;详情
Accept-Charset——客户端可以处理的字符集类型;详情
Accept-Encoding——客户端能够理解的内容编码方式;详情
Accept-Language——客户端可以理解的自然语言;详情
Authorization——Web 认证信息;详情
Cookie——通过Set-Cookie设置的值;详情
DNT——表明用户对于网站追踪的偏好;详情
From——用户的电子邮箱地址;详情
Host——请求资源所在服务器;详情
If-Match——比较实体标记(ETag);详情
If-Modified-Since——比较资源的更新时间;详情
If-None-Match——比较实体标记(与 If-Match 相反);详情
If-Range——资源未更新时发送实体 Byte 的范围请求;详情
If-Unmodified-Since——比较资源的更新时间(与 If-Modified-Since 相反);详情
Origin——表明了请求来自于哪个站点;详情
Proxy-Authorization——代理服务器要求客户端的认证信息;详情
Range——实体的字节范围请求;详情
Referer——对请求中 URI 的原始获取方;详情
TE——指定用户代理希望使用的传输编码类型;详情
Upgrade-Insecure-Requests——表示客户端优先选择加密及带有身份验证的响应;详情
User-Agent——浏览器信息;详情

3、Response Headers

Accept-Ranges——是否接受字节范围请求;详情
Age——消息对象在缓存代理中存贮的时长,以秒为单位;详情
Clear-Site-Data——表示清除当前请求网站有关的浏览器数据(cookie,存储,缓存);详情
Content-Security-Policy——允许站点管理者在指定的页面控制用户代理的资源;详情
Content-Security-Policy-Report-Only—— 详情
ETag——资源的匹配信息;链接描述
Location——令客户端重定向至指定 URI;详情
Proxy-Authenticate——代理服务器对客户端的认证信息;详情
Public-Key-Pins——包含该Web 服务器用来进行加密的 public key (公钥)信息;详情
Public-Key-Pins-Report-Only——设置在公钥固定不匹配时,发送错误信息到report-uri;详情
Referrer-Policy——用来监管哪些访问来源信息——会在 Referer 中发送;详情
Server——HTTP 服务器的安装信息;详情
Set-Cookie——服务器端向客户端发送 cookie;详情
Strict-Transport-Security——它告诉浏览器只能通过HTTPS访问当前资源;详情
Timing-Allow-Origin——用于指定特定站点,以允许其访问Resource Timing API提供的相关信息;详情
Tk——显示了对相应请求的跟踪情况;详情
Vary——服务器缓存的管理信息;详情
WWW-Authenticate——定义了使用何种验证方式去获取对资源的连接;详情
X-XSS-Protection——当检测到跨站脚本攻击 (XSS)时,浏览器将停止加载页面;详情

4、Entity Headers

Allow——客户端可以处理的内容类型,这种内容类型用MIME类型来表示;详情
Content-Encoding——用于对特定媒体类型的数据进行压缩;详情
Content-Language——访问者希望采用的语言或语言组合;详情
Content-Length——发送给接收方的消息主体的大小;详情
Content-Location——替代对应资源的 URI;详情
Content-Range——实体主体的位置范围;详情
Content-Type——告诉客户端实际返回的内容的内容类型;详情
Expires——包含日期/时间, 即在此时候之后,响应过期;详情
Last-Modified——资源的最后修改日期时间;详情

三、HTTP 具体应用

1、Cookie

HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。

Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时自动被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。

Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。

a、创建过程

服务器发送的响应报文包含Set-Cookie首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: PHPSESSID=kq8v6iujarsgflkeq7shmai9c7

客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。

GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: PHPSESSID=kq8v6iujarsgflkeq7shmai9c7

b、分类

会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。
持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。
安全 Cookie:指定HttpOnly,这样cookie不能使用 JavaScript 经由 Document.cookie 属性,来防范跨站脚本攻击(XSS)。
HTTPS Cookie: 指定Secure,只有在请求使用SSL和HTTPS协议的时候才会被发送到服务器。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;HttpOnly
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;Secure

2、缓存

降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存(但是只能缓存get,不能缓存其他类型请求)。
cache FAQ MDN

流程图:
clipboard.png

a、判断cache-control或者expires是否有效

clipboard.png

max-age值为缓存的毫秒数。

可以看到response-headers中设置了cache-control,并大于0,则下次直接从缓存(from disk cache)中获取

b、Etag判断

当发现Cache-Control设置的毫秒数过期了,则直接发送请求:
——如果响应中包含Etag(服务器生成的值),则浏览器再次向web服务器发送请求并带上头If-None-Match(Etag的值),web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定返回200或304(浏览器会从缓存中获取)。
——如果响应中不包含Etag,则进行Last-Modified判断

c、Last-Modified判断

当发现response header中含有Last-Modified,则再次向web服务器请求时带上头 If-Modified-Since,web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对,然后服务器决定返回200或者304(缓存)

浏览器强制告诉服务器不缓存资源:

//request headers
Cache-Control:max-age=0, no-cache

四、HTTP 请求 method

五、HTTP 状态码

clipboard.png

1、1XX

  • 100(continue) 表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。

2、2XX

  • 200(OK) 表示从客户端发来的请求在服务器端被正常处理了。
  • 204(No Content) 该状态码代表服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。
  • 206(Partial Content) 该状态码表示客户端进行了范围请求,而服务器成功执行了这部分的 GET 请求。响应报文中包含由 Content-Range 指定范围的实体内容。

3、3XX

  • 301(Moved Permanently) 永久性重定向。该状态码表示请求的资源已被分配了新的 URI,以后应使用资源现在所指的 URI。
  • 302(Found) 临时性重定向。比如在没有登录情况下访问网站"个人中心",会重定向到登录页,但是你登录后,访问个人中心时,它又不会重定向到其他地方了。
  • 303(See Other) 和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
  • 304(Not Modified) 如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。

4、4XX

  • 400(Bad Request) 该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。
  • 401 Unauthorized 该状态码表示发送的请求需要有认证信息。返回含有 401 的响应必须包含一个适用于被请求资源的 WWW-Authenticate 首部用以询问用户信息。当浏览器初次接收到 401 响应,会弹出认证用的对话窗口。第二次接收到,则不弹出,直接表示认证失败。
  • 403(Forbidden) 对请求资源的访问被服务器拒绝了,一般是未获得文件系统的访问授权,问权限出现某些问题。
  • 404(Not Found) 浏览器地址错误。服务器找不到对应资源。

5、5XX

  • 500(Internal Server Error) 服务器在执行时报错。
  • 503(Service Unavailable) 服务器暂时处于超负载或正在进行停机维护,无响应。一般需要重启服务器即可。

六、MIME类型

浏览器通常使用MIME类型(而不是文件扩展名)来确定如何处理文档;因此设置正确的MIME类型附加到headers是非常重要的。
clipboard.png
除了上面的基本的5中类型外,还有一种类型,即multipart类型。

multipart/form-data 在表单中通过post上传
multipart/byteranges (不常用,不知道)

一般的,如果没有显示的在request headers的Allow上设置类型,浏览器的MIME 嗅探会推测出来对应的类型,如果发现找不到特定的子类型,则给出默认类型,比如对于text文件类型若没有特定的subtype,就使用 text/plain。类似的,二进制文件没有特定或已知的 subtype,即使用 application/octet-stream。

七、HTTP 使用的认证方式

1、BASIC 认证(基本认证)
2、DIGEST 认证(摘要认证)
3、SSL 客户端认证
4、FormBase 认证(基于表单认证)

1、BASIC 认证

当在浏览器端输入一个url时,会自动弹出一个框,要求输入用户名和密码。此种方式为basic认证。
下面是认证执行过程:
第一步:在浏览器地址栏中输入 http://localhost:8080/auth
第二步: 服务器执行,发现需要认证,返回这个请求的响应。并在response headers中添加WWW-Authenticate,将http请求状态设置为401.
clipboard.png
浏览器检测到WWW-Authenticate为basic后,自动弹出框。
clipboard.png
第三步: 当用户看到框后,输入 用户名和密码,浏览器会将用户名和密码通过base64方式编码,然后添加到 request headers的 Authorization 中发送给服务器,浏览器验证通过,返回200状态码
clipboard.png
如果验证不通过,则继续返回状态码401,提示验证失败。
clipboard.png

缺点:
BASIC 认证虽然采用 Base64 编码方式,但这不是加密处理。不需要任何附加信息即可对其解码。换言之,由于明文解码后就是用户 ID和密码,在 HTTP 等非加密通信的线路上进行 BASIC 认证的过程中,如果被人窃听,被盗的可能性极高。

2、DIGEST 认证

为了弥补Basic认证没有加密所带来的不安全性,出现了DIGEST 认证。
过程如下:
第一步:在浏览器地址栏中输入 http://localhost:8080/auth
第二步: 服务器执行,发现需要认证,返回这个请求的响应。并在response headers中添加WWW-Authenticate(包含有随机码nonce),将http请求状态设置为401.

浏览器检测到WWW-Authenticate为 digest 后,自动弹出框。

第三步: 当用户看到框后,输入 用户名和密码,浏览器会将用户名和上步返回的nouce,添加到 request headers的 Authorization 中,同时也将经过 MD5 运算后的密码字符串,生成key为response,一并添加到Authorization 中。至此请求的request headers的Authorization 包含有如下信息。

//request header
Authorization: Digest username="my name",realm="DIGEST",nounce="xxxxxxxxxxx",algorithm="MD5",response="xxxxxxxxxxxxx"

然后发送给服务器,浏览器验证通过,返回200状态码。

如果验证不通过,则继续返回状态码401,提示验证失败。

缺点: 虽然 DIGEST 认证提供了高于 BASIC 认证的安全等级, DIGEST 认证提供防止密码被窃听的保护机制,但并不存在防止用户伪装的保护机制,仍达不到多数 Web 网站对高度安全等级的追求标准。

3、SSL 客户端认证

对于 BASIC 认证和 DIGEST 认证来说,只要输入的用户名和密码正确,即可认证是本人的行为。但如果用户名和密码被盗,就很有可能被第三者冒充。

而利用 SSL客户端认证则可以避免该情况的发生。在SSL认证时,必须使用https协议。

由于SSL中的各种加密和秘钥算法过于复杂,有兴趣的可以直接阅读SSL相关书籍,本文忽略详细过程。

4、FormBase 认证

基于表单认证的标准规范尚未有定论,一般会使用 Cookie 来管理。
认证过程:
第一步:当用户在浏览器的登录页面,输入用户名和密码,通过http请求发送给后端。
第二步:后端保存用户的信息到session中,并返回sessionId, 通过http添加到response headers的Set-cookie中

//response headers
Set-cookie: PHPSESSID=kq8v6iujarsgflkeq7shmai9c7

然后浏览器成功登录,并跳转页面。
第三步:当用户访问个人中心或者其他页面时。http请求的request header中会自动携带cookie

//request headers
Cookie: PHPSESSID=kq8v6iujarsgflkeq7shmai9c7

这样,服务端会认为是你本人在操作。

但是如果攻击者通过“跨站脚本攻击(XSS)”,通过docuemnt.cookie来获取cookie,则sessionID很容易被盗。
为减轻XSS造成的损失,可以事先在 Set-cookie内加上 httponly 属性,这样就禁止了docuemnt.cookie操作。

Set-cookie: PHPSESSID=kq8v6iujarsgflkeq7shmai9c7, httponly 

八、跨域资源共享(CORS)

是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

1、简单请求(不会触发CORS预检请求)

需要满足下列所有条件:
第一条: 请求方式必须为 GET | HEAD | POST
第二条: Content-Type 的值必须属于下列之一:application/x-www-form-urlencoded | multipart/form-data | text/plain
第三条: 在请求中,不会发送自定义的头部(如X-Modified)

例如: 在http://foo.exmaple上要访问 http://bar.other上的资源。则

//request headers上添加
Origin: http://foo.example
//response headers返回
Access-Control-Allow-Origin: *

由于在 http://foo.example 上要访问 http://bar.other,所以http://bar.other必须要告诉其...,能不能访问我。*号表示该资源可以被任意外域访问。

如果返回

Access-Control-Allow-Origin: http://foo.example

表示,http://bar.other的资源只能被http://foo.example访问,其他网站不能访问我。

2、非“简单请求”(触发CORS预检请求)

满足下列条件之一:
第一条: http请求方式为下列:PUT | DELETE | CONNECT | OPTIONS | TRACE | PATCH
第二条: Content-Type 的值不属于下列之一: application/x-www-form-urlencoded | multipart/form-data | text/plain
第三条: 在请求中,发送自定义的头部(如X-Modified)

如果在 http://foo.exmaple 上要访问 http://bar.other/resources/po... 上的资源。且 request headers 中 Content-Type为application/xml,请求method为post。
那么此请求是个“非简单请求”。首先浏览器会自动发送带有options选项的预检请求,然后发送实际请求

//预检请求request headers
OPTIONS /resources/post-here/ HTTP/1.1(自动,不需要设置)
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
//预检请求返回response headers
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

Access-Control-Allow-Origin 表明服务器允许任何其他服务器访问自己。
Access-Control-Allow-Methods 表明服务器允许客户端使用 POST, GET, OPTIONS 方法发起请求。
Access-Control-Allow-Headers 表明服务器允许请求中携带字段 X-PINGOTHER, Content-Type。
Access-Control-Max-Age 表明在86400内,不会再发送预检请求。

然后浏览器接着发送实际请求

POST /resources/post-here/ HTTP/1.1
Content-Type: application/xml; charset=UTF-8

实际请求返回

HTTP/1.1 200 OK
Access-Control-Allow-Origin: * 

3、附带身份凭证的请求

一般而言,对于跨域 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位。

var xhr = new XMLHttpRequest();
var url = 'http://xxxxxxxxx';
    
xhr.open('GET', url, true);
xhr.withCredentials = true;  // 设置发送人请求时 携带 cookie凭证
xhr.send(); 

请求发送

GET /resources/access-control-with-credentials/ HTTP/1.1
Cookie: pageAccess=2

请求返回

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://foo.example  //在携带凭证的请求中,返回不得设置为*, 必须设置为具体域名
Access-Control-Allow-Credentials: true  //必须携带这个,否则响应内容不会返回给请求的发起者

4、CORS几个response header解释

4.1 Access-Control-Allow-Origin
它的值只有两种,要么*, 好么具体的域名 <origin>

4.2 Access-Control-Expose-Headers
在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头:
Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma

无法访问其他的响应头(甚至在控制台network中看不到),如 set-cookie等,如果要访问其他头,则需要服务器设置,将能返回的响应头放入白名单

Access-Control-Expose-Headers: set-cookie

这样浏览器就能够通过getResponseHeader访问set-cookie响应头了

4.3 Access-Control-Max-Age
指定了预检请求的结果能够被缓存多久,在此时间内,不会再发起预检请求。

Access-Control-Max-Age: <seconds>

4.4 Access-Control-Allow-Credentials
指定了当浏览器的credentials设置为true时,是否允许浏览器读取response的内容。

查看原文

赞 32 收藏 24 评论 0

lllllxt_in_sf 赞了文章 · 3月18日

前端面试:谈谈 JS 垃圾回收机制

个人专栏 ES6 深入浅出已上线,深入ES6 ,通过案例学习掌握 ES6 中新特性一些使用技巧及原理,持续更新中,←点击可订阅。

点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。


为了保证的可读性,本文采用意译而非直译。

最近看到一些面试的回顾,不少有被面试官问到谈谈JS 垃圾回收机制,说实话,面试官会问这个问题,说明他最近看到一些关于 JS 垃圾回收机制的相关的文章,为了 B 格,就会顺带的问问。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

最近看到一篇讲 JS 垃圾回收的国外文章,觉得讲得明白,所以就翻译过来了,希望对你们有所帮助。

垃圾回收

JavaScript 中的内存管理是自动执行的,而且是不可见的。我们创建基本类型、对象、函数……所有这些都需要内存。

当不再需要某样东西时会发生什么? JavaScript 引擎是如何发现并清理它?

可达性

JavaScript 中内存管理的主要概念是可达性。

简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中。

1. 有一组基本的固有可达值,由于显而易见的原因无法删除。例如:

  • 本地函数的局部变量和参数
  • 当前嵌套调用链上的其他函数的变量和参数
  • 全局变量
  • 还有一些其他的,内部的

这些值称为根。

2. 如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的。

例如,如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的,详细的例子如下。

JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象。

一个简单的例子

下面是最简单的例子:

// user 具有对象的引用
let user = {
  name: "John"
};

图片描述

这里箭头表示一个对象引用。全局变量“user”引用对象 {name:“John”} (为了简洁起见,我们将其命名为John)。John 的 “name” 属性存储一个基本类型,因此它被绘制在对象中。

如果 user 的值被覆盖,则引用丢失:

user = null;

图片描述

现在 John 变成不可达的状态,没有办法访问它,没有对它的引用。垃圾回收器将丢弃 John 数据并释放内存。

两个引用

现在让我们假设我们将引用从 user 复制到 admin:

// user具有对象的引用
let user = {
  name: "John"
};

let admin = user;

图片描述

现在如果我们做同样的事情:

user = null;

该对象仍然可以通过 admin 全局变量访问,所以它在内存中。如果我们也覆盖admin,那么它可以被释放。

相互关联的对象

现在来看一个更复杂的例子, family 对象:

function marry (man, woman) {
  woman.husban = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
})

函数 marry 通过给两个对象彼此提供引用来“联姻”它们,并返回一个包含两个对象的新对象。

产生的内存结构:

图片描述

到目前为止,所有对象都是可访问的。

现在让我们删除两个引用:

delete family.father;
delete family.mother.husband;

图片描述

仅仅删除这两个引用中的一个是不够的,因为所有对象仍然是可访问的。

但是如果我们把这两个都删除,那么我们可以看到 John 不再有传入的引用:

图片描述

输出引用无关紧要。只有传入的对象才能使对象可访问,因此,John 现在是不可访问的,并将从内存中删除所有不可访问的数据。

垃圾回收之后:

图片描述

无法访问的数据块

有可能整个相互连接的对象变得不可访问并从内存中删除。

源对象与上面的相同。然后:

family = null;

内存中的图片变成:

图片描述

这个例子说明了可达性的概念是多么重要。

很明显,John和Ann仍然链接在一起,都有传入的引用。但这还不够。

“family”对象已经从根上断开了链接,不再有对它的引用,因此下面的整个块变得不可到达,并将被删除。

内部算法

基本的垃圾回收算法称为“标记-清除”,定期执行以下“垃圾回收”步骤:

  • 垃圾回收器获取根并“标记”(记住)它们。
  • 然后它访问并“标记”所有来自它们的引用。
  • 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
  • 以此类推,直到有未访问的引用(可以从根访问)为止。
  • 除标记的对象外,所有对象都被删除。

例如,对象结构如下:

图片描述

我们可以清楚地看到右边有一个“不可到达的块”。现在让我们看看“标记并清除”垃圾回收器如何处理它。

第一步标记根

图片描述

然后标记他们的引用

图片描述

以及子孙代的引用:

图片描述

现在进程中不能访问的对象被认为是不可访问的,将被删除:

图片描述

这就是垃圾收集的工作原理。JavaScript引擎应用了许多优化,使其运行得更快,并且不影响执行。

一些优化:

  • 分代回收——对象分为两组:“新对象”和“旧对象”。许多对象出现,完成它们的工作并迅速结 ,它们很快就会被清理干净。那些活得足够久的对象,会变“老”,并且很少接受检查。
  • 增量回收——如果有很多对象,并且我们试图一次遍历并标记整个对象集,那么可能会花费一些时间,并在执行中会有一定的延迟。因此,引擎试图将垃圾回收分解为多个部分。然后,各个部分分别执行。这需要额外的标记来跟踪变化,这样有很多微小的延迟,而不是很大的延迟。
  • 空闲时间收集——垃圾回收器只在 CPU 空闲时运行,以减少对执行的可能影响。

面试怎么回答

1)问什么是垃圾

一般来说没有被引用的对象就是垃圾,就是要被清除, 有个例外如果几个对象引用形成一个环,互相引用,但根访问不到它们,这几个对象也是垃圾,也要被清除。

2)如何检垃圾

一种算法是标记 标记-清除 算法,还想说出不同的算法可以参考这里

更深入一些的讲解 http://newhtml.net/v8-garbage...

还有一种牛逼的答法就是说看我的博客,当然是要自己总结的博客。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

查看原文

赞 213 收藏 130 评论 9

lllllxt_in_sf 赞了文章 · 2月25日

实时最新中国省市区县街道级geoJSON格式地图数据Echarts地图数据联动数据下载

发现个可以免费下载全国 geojson 数据的网站,推荐一下。支持全国、省级、市级、区/县级、街道/乡镇级以及各级的联动数据

geojson 数据下载地址:https://hxkj.vip/demo/echartsMap/

该项目 github 地址:https://github.com/TangSY/echarts-map-demo,喜欢的话,可以给个star哦

一、通过API接口,实时获取最新中国省市区县街道级乡镇级geoJSON格式地图数据,可用于Echarts地图展示

image
image

二、通过获取到的数据整理了一系列联动数据,每天自动更新,免费下载

image

查看原文

赞 3 收藏 1 评论 0

lllllxt_in_sf 关注了问题 · 2020-11-09

benchmark是什么东西?

benchmark是什么东西? 看到好多项目里面都有一个benchmark

关注 4 回答 2

lllllxt_in_sf 关注了问题 · 2020-10-22

vue+less 页面和系统自定义主题怎么样做?

@import './theme.less';

.theme();
@background-color:#373e41;

.theme-blue{
.theme(#3355aa, #4371e2, #ebebeb, #e5e5e5, rgba(0,0,0,0.1), rgba(67,113,226,0.1), rgba(67,113,226,0.3));//默认的样式
@background-color:#000
}
像这样可以切换,但是组件内也有样式,就不好改,
如果改配置文件的话,体验就差点了

关注 3 回答 0

lllllxt_in_sf 回答了问题 · 2020-10-21

vue中 如何让data里面的一个对象失去响应式,都有哪些方法?

为什么会有这么另类的需求?

关注 3 回答 2

lllllxt_in_sf 发布了文章 · 2020-09-27

浏览器标签页之间通信之 -- Localstorage+IndexedDB

推荐一个最近写的工具 @lllllxt/storage-idb-message,用于同源下浏览器标签之间的通讯, 相比纯用Localstorage做通讯,这个插件可以传递更大的数据。

文档:

storage-idb-message

基于 LocalStorage + IndexedDB 封装的消息传递的模块,在同源(不跨域)的前提下,用于同一页面多个 iframe、跨 tab 页面、移动端不同 webview 页面之间的消息传递

做这玩意的初衷

本来我是只用 LocalStorage 做消息传递的, 但后来某次在 Chrome 上传递的数据>5M 导致数据丢失,于是乎就有了用 LocalStorage 做指令,IndexedDB 做数据存储这个想法了

参考:

LocalStorage 储存空间在 2.5MB 到 10MB 之间(各家浏览器不同),而 IndexedDB 比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。

通过 npm 安装

npm i @lllllxt/storage-idb-message
import StorageIdbMessage from '@lllllxt/storage-idb-message'

const StorageIdbMessage = request('@lllllxt/storage-idb-message')

const _SIM = new StorageIdbMessage(opts: Opts)

// 监听指令
_SIM.response = (orderName, data) => {
    console.log(orderName, data)
})

// 发送指令
_SIM.send(YourOrder, AnyData)

通过<script>标签引用

storage-idb-message.min.js

此方法是向 window 对象中注册一个 StorageIdbMessage 对象

参数(Opts)

参数描述
storageKey指令的 LocalStorage key 名称
clearIdb默认 true,是否在 response 后清除 IndexedDB 的数据
force默认 false,调用 clearCache 清除缓存

方法

方法描述
send(order: String, data: any)发送
response(order: String, data: any)响应其他页面传过来的指令

静态方法

方法描述
clearCache(successFn?: Function, errFn?: Function)清除 indexedDB 缓存

BTW ------------------------------------

由于开发初衷是为了在vue工程上使用, 于是同时也写了基于这个工具开发的vue插件 @lllllxt/vue-storage-idb-message

查看原文

赞 0 收藏 0 评论 0

lllllxt_in_sf 发布了文章 · 2020-09-17

webpack4 + babel7 配置ie兼容

有一说一, IE真是让人头大?‍♂️?‍♂️
以下只给出如何配置, 如果想了解更多请查看文末传送门

安装:
npm i @babel/polyfill -S
npm i @babel/preset-env @babel/plugin-transform-runtime -D

// 根据babel.config.json中corejs 2 / 3, 自行选择安装 @babel/runtime-corejs2 或 @babel/runtime-corejs3
npm i @babel/runtime-corejs3 -D
webpack.config.js
 entry: {
  main: ['@babel/polyfill', './main.js']
 }
babel.config.json
"presets": ["@babel/preset-env"],
"plugins": [
    [
        "@babel/plugin-transform-runtime",
        {
            "corejs": 3
        }
    ] 
]

----------------------- 顺便说一下 ------------------------

开发环境下, 在ie上可能一片空白, 有以下三种方案

  1. build后在ie上调试 [ 可行, 但太麻烦了 ]
  2. devServer inline设置成false [ 可行, 但是iframe模式下, HRM会失效, 需要手动刷新页面 ]
  3. 降低devServer版本到@2.11.1 [ 没试过 ]

再说一遍, IE真真真是让人头大 ???

相关资料转送:
@babel/polyfill
@babel/plugin-transform-runtime
preset-env与plugin-transform-runtime 使用及场景区别

查看原文

赞 1 收藏 1 评论 0

lllllxt_in_sf 赞了文章 · 2020-09-17

@babel/preset-env 与@babel/plugin-transform-runtime 使用及场景区别

之前在用babel 的时候有个地方一直挺晕的,@babel/preset-env@babel/plugin-transform-runtime都具有转换语法的能力, 并且都能实现按需 polyfill ,但是网上又找不到比较明确的答案, 趁这次尝试 roullp 的时候试了试.

如果我们什么都不做, 没有为babel 编写参数及配置, 那babel 并没有那么大的威力, 它什么都不会做, 正是因为各个预设插件的灵活组合、赋能, 让 babel 充满魅力, 创造奇迹

首先是 @babel/preset-env

@babel/preset-env

这是一个我们很常用的预设, 几乎所有的教程和框架里都会让你配置它, 它的出现取代了 preset-es20** 系列的babel 预设, 你再也不需要繁杂的兼容配置了。 每出一个新提案就加一个? 太蠢了。

有了它, 我们就可以拥有全部, 并且! 它还可以做到按需加载我们需要的 polyfill。 就是这么神奇。
但是吧, 它也不是那么自动化的, 如果你要是不会配置,很有可能就没有用起它的功能

不管怎么养, 首先试一下,眼见为实

首先创建一个 index.js ,内容如下, 很简单

function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
console.log(arr)

然后我们在根目录下创建一个 .babelrc文件, 帮我们刚刚说的预设加进去

{
  "presets": [
    ["@babel/preset-env"]
  ]
}

然后我我们打包一下(这里我用的是roullup)

看一下产出的结果
2019-12-03-21-00-52

我们可以看到, 它babel帮我们做了这几件事情:

  1. 转换箭头函数
  2. const 变为 var

奇怪, 为什么 babel 不帮我们转换 map ? 还有 promise 这些也都是es6的特性呀

嗯~,会不会是我们的目标浏览器不对, babel 觉得不需要转换了, 会不会是这样, 那我们加一个 .browserslistrc 试一下

那就。让我们在根目录下创建一个 .browserslistrc
2019-12-03-21-06-54

好。现在让我们再打包一次.
2019-12-03-21-07-43

咦, 没什么效果。 跟刚刚一样啊。 说明不是目标浏览器配置的问题, 是babel 做不了这个事。

因为默认 @babel/preset-env 只会转换语法,也就是我们看到的箭头函数、const一类。
如果进一步需要转换内置对象、实例方法,那就得用polyfill, 这就需要你做一点配置了,

这里有一个至关重要的参数 "useBuiltIns",他是控制 @babel/preset-env 使用何种方式帮我们导入 polyfill 的核心, 它有三个值可以选

entry

这是一种入口导入方式, 只要我们在打包配置入口 或者 文件入口写入 import "core-js" 这样一串代码, babel 就会替我们根据当前你所配置的目标浏览器(browserslist)来引入所需要的polyfill 。

像这样, 我们在 index.js 文件中加入试一下core-js

// src/index.js
import "core-js";
function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
console.log(arr)

babel配置如下

[
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "entry"
      }
    ]
  ]
}

当前 .browserslistrc 文件(更改目标浏览器为 Chrome 是为了此处演示更直观,简洁), 我们只要求兼容 chrome 50版本以上即可(当下最新版本为78)

Chrome > 50

那打包后如何呢?
2019-12-03-21-46-14

恐怖如斯啊,babel把我们填写的 import "core-js"替换掉, 转而导入了一大片的polyfill, 而且都是一些我没有用到的东西。

那我们提升一下目标浏览器呢? 它还会导入这么多吗?
此时, 我们把目标浏览器调整为比较接近最新版本的 75(当下最新版本为78)

// .browserslistrc

Chrome > 75

此刻打包后引入的 polyfill 明显少了好多。
2019-12-03-21-51-46

但同样是我们没用过的。
这也就是印证了上面所说的, 当 useBuiltIns 的值为 entry 时, @babel/preset-env 会按照你所设置的目标浏览器在入口处来引入所需的 polyfill,
不管你需不需要。

如此,我们可以知道, useBuiltIns = entry 的优点是覆盖面积就比较广, 一股脑全部搞定, 但是缺点就是打出来的包就大了多了很多没有用到的 polyfill, 并且还会污染全局

useage

这个就比较神奇了, useBuiltIns = useage 时,会参考目标浏览器(browserslist) 和 代码中所使用到的特性来按需加入 polyfill

当然, 使用 useBuiltIns = useage, 还需要填写另一个参数 corejs 的版本号,

core-js 支持两个版本, 2 或 3, 很多新特性已经不会加入到 2 里面了, 比如: flat 等等最新的方法, 2 这个版本里面都是没有的, 所以建议大家用3

此时的 .babelrc

{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

此时的 index.js


function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
const hasNumber = (num) => [4, 5, 6, 7, 8].includes(num)
console.log(arr)
console.log( hasNumber(2) )

此时的 .browserslistrc

> 1%
last 10 versions
not ie <= 8

打包后:
2019-12-03-22-59-03

nice ,够神奇, 我们用的几个新特性真的通通都加上了

这种方式打包体积不大,但是如果我们排除node_modules/目录,遇上没有经过转译的第三方包,就检测不到第三方包内部的 ‘hello‘.includes(‘h‘)这种句法,这时候我们就会遇到bug

false

剩下最后一个 useBuiltIns = false , 那就简单了, 这也是默认值 , 使用这个值时不引入 polyfill

@babel/runtime

这种方式会借助 helper function 来实现特性的兼容,
并且利用 @babel/plugin-transform-runtime 插件还能以沙箱垫片的方式防止污染全局, 并抽离公共的 helper function , 以节省代码的冗余

也就是说 @babel/runtime 是一个核心, 一种实现方式, 而 @babel/plugin-transform-runtime 就是一个管家, 负责更好的重复使用 @babel/runtime

@babel/plugin-transform-runtime 插件也有一个 corejs 参数需要填写

版本2 不支持内置对象 , 但自从Babel 7.4.0 之后,拥有了 @babel/runtime-corejs3 , 我们可以放心使用 corejs: 3 对实例方法做支持

当前的 .babelrc

{
  "presets": [
    ["@babel/preset-env"]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

当前的 index.js

function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
const hasNumber = (num) => [4, 5, 6, 7, 8].includes(num)
console.log(arr)
console.log( hasNumber(2) )

打包后如下:
2019-12-04-00-01-39

我们看到使用 @babel/plugin-transform-runtime 编译后的代码和之前的 @babel/preset-env 编译结果大不一样了,
它使用了帮助函数, 并且赋予了别名 , 抽出为公共方法, 实现复用。 比如它用了 _Promise 代替了 new Promise , 从而避免了创建全局对象

上面两种方式一起用会怎么样

useage 和 @babel/runtime

useage 和 @babel/runtime 同时使用的情况下比较智能, 并没有引入重复的 polyfill

个人分析原因应该是: babel 的 plugin 比 prset 要先执行, 所以preset-env 得到了 @babel/runtime 使用帮助函数包装后的代码,而 useage 又是检测代码使用哪些新特性来判断的, 所以它拿到手的只是一堆 帮助函数, 自然没有效果了

实验过程如下:

当前index.js


function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
const hasNumber = (num) => [4, 5, 6, 7, 8].includes(num)
const hasNumber2 = (num) => [4, 5, 6, 7, 8, 9].includes(num)
console.log(arr)
console.log( hasNumber(2))
console.log( hasNumber2(3) )

当前 .babelrc

{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

打包结果:

2019-12-04-00-23-24

entry 和 @babel/runtime

跟 useage 的情况不一样, entry 模式下, 在经过 @babel/runtime 处理后不但有了各种帮助函数还引入了许多polyfill, 这就会导致打包体积无情的增大

个人分析: entry 模式下遭遇到入口的 import "core-js" 及就立即替换为当前目标浏览器下所需的所有 polyfill, 所以也就跟 @babel/runtime 互不冲突了, 导致了重复引入代码的问题, 所以这两种方式千万不要一起使用, 二选一即可

实现过程如下:

当前 index.js:

import "core-js"
function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
const hasNumber = (num) => [4, 5, 6, 7, 8].includes(num)
const hasNumber2 = (num) => [4, 5, 6, 7, 8, 9].includes(num)
console.log(arr)
console.log( hasNumber(2))
console.log( hasNumber2(3) )

当前 .babelrc

{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "entry"
      }
    ]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

当前 .browserslistrc 的目标版本(为了减少打包后的文件行数为又改为chrome 了, 懂那个意思就行)

Chrome > 70

打包结果:

2019

总结

  1. @babel/preset-env 拥有根据 useBuiltIns 参数的多种polyfill实现,优点是覆盖面比较全(entry), 缺点是会污染全局, 推荐在业务项目中使用

    • entry 的覆盖面积全, 但是打包体积自然就大,
    • useage 可以按需引入 polyfill, 打包体积就小, 但如果打包忽略node_modules 时如果第三方包未转译则会出现兼容问题
  2. @babel/runtime 在 babel 7.4 之后大放异彩, 利用 corejs 3 也实现了各种内置对象的支持, 并且依靠 @babel/plugin-transform-runtime 的能力,沙箱垫片和代码复用, 避免帮助函数重复 inject 过多的问题, 该方式的优点是不会污染全局, 适合在类库开发中使用

    上面 1, 2 两种方式取其一即可, 同时使用没有意义, 还可能造成重复的 polyfill 文件

查看原文

赞 39 收藏 18 评论 15

认证与成就

  • 获得 8 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2017-09-13
个人主页被 759 人浏览