webpack 层面如何做性能优化
优化前的筹备工作
- 筹备基于工夫的剖析工具:咱们须要一类插件,来帮忙咱们统计我的项目构建过程中在编译阶段的耗时状况。
speed-measure-webpack-plugin
剖析插件加载的工夫 - 应用
webpack-bundle-analyzer
剖析产物内容
代码优化:
无用代码打消,是许多编程语言都具备的优化伎俩,这个过程称为 DCE (dead code elimination),即 删除不可能执行的代码;
例如咱们的 UglifyJs
,它就会帮咱们在生产环境中删除不可能被执行的代码,例如:
var fn = function() {
return 1;
// 上面代码便属于 不可能执行的代码;// 通过 UglifyJs (Webpack4+ 已内置) 便会进行 DCE;var a = 1;
return a;
}
摇树优化 (Tree-shaking),这是一种形象比喻。咱们把打包后的代码比喻成一棵树,这里其实示意的就是,通过工具 “ 摇 ” 咱们打包后的 js 代码,将没有应用到的无用代码 “ 摇 ” 下来 (删除)。即 打消那些被 援用了但未被应用 的模块代码。
- 原理: 因为是在编译时优化,因而最根本的前提就是语法的动态剖析,ES6 的模块机制 提供了这种可能性。不须要运行时,便可进行代码字面上的动态剖析,确定相应的依赖关系。
-
问题: 具备 副作用 的函数无奈被
tree-shaking
- 在援用一些第三方库,须要去察看其引入的代码量是不是合乎预期;
- 尽量写纯函数,缩小函数的副作用;
- 可应用
webpack-deep-scope-plugin
,能够进行作用域剖析,缩小此类情况的产生,但仍须要留神;
code-spliting: 代码宰割技术,将代码宰割成多份进行 懒加载 或 异步加载,防止打包成一份后导致体积过大,影响页面的首屏加载;
Webpack
中应用SplitChunksPlugin
进行拆分;- 按 页面 拆分: 不同页面打包成不同的文件;
-
按 性能 拆分:
- 将相似于播放器,计算库等大模块进行拆分后再懒加载引入;
- 提取复用的业务代码,缩小冗余代码;
- 按 文件批改频率 拆分: 将第三方库等不常批改的代码独自打包,而且不扭转其文件 hash 值,能最大化使用浏览器的缓存;
scope hoisting : 作用域晋升,将扩散的模块划分到同一个作用域中,防止了代码的反复引入,无效缩小打包后的代码体积和运行时的内存损耗;
编译性能优化:
- 降级至 最新 版本的
webpack
,能无效晋升编译性能; -
应用
dev-server
/ 模块热替换 (HMR
) 晋升开发体验;- 监听文件变动 疏忽 node_modules 目录能无效进步监听时的编译效率;
-
放大编译范畴
modules
: 指定模块门路,缩小递归搜寻;mainFields
: 指定入口文件形容字段,缩小搜寻;noParse
: 防止对非模块化文件的加载;includes/exclude
: 指定搜寻范畴 / 排除不必要的搜寻范畴;alias
: 缓存目录,防止反复寻址;
-
babel-loader
- 疏忽
node_moudles
,防止编译第三方库中曾经被编译过的代码 - 应用
cacheDirectory
,能够缓存编译后果,防止多次重复编译
- 疏忽
-
多过程并发
webpack-parallel-uglify-plugin
: 可多过程并发压缩 js 文件,进步压缩速度;HappyPack
: 多过程并发文件的Loader
解析;
-
第三方库模块缓存:
DLLPlugin
和DLLReferencePlugin
能够提前进行打包并缓存,防止每次都从新编译;
-
应用剖析
Webpack Analyse / webpack-bundle-analyzer
对打包后的文件进行剖析,寻找可优化的中央- 配置 profile:true,对各个编译阶段耗时进行监控,寻找耗时最多的中央
-
source-map
:- 开发:
cheap-module-eval-source-map
- 生产:
hidden-source-map
;
- 开发:
优化 webpack 打包速度
-
缩小文件搜寻范畴
- 比方通过别名
loader
的test
,include & exclude
Webpack4
默认压缩并行Happypack
并发调用babel
也能够缓存编译Resolve
在构建时指定查找模块文件的规定- 应用
DllPlugin
,不必每次都从新构建 -
externals
和DllPlugin
解决的是同一类问题:将依赖的框架等模块从构建过程中移除。它们的区别在于- 在 Webpack 的配置方面,
externals
更简略,而DllPlugin
须要独立的配置文件。 DllPlugin
蕴含了依赖包的独立构建流程,而externals
配置中不蕴含依赖框架的生成形式,通常应用已传入 CDN 的依赖包externals
配置的依赖包须要独自指定依赖模块的加载形式:全局对象、CommonJS、AMD 等- 在援用依赖包的子模块时,
DllPlugin
毋庸更改,而externals
则会将子模块打入我的项目包中
- 在 Webpack 的配置方面,
优化打包体积
- 提取第三方库或通过援用内部文件的形式引入第三方库
- 代码压缩插件
UglifyJsPlugin
- 服务器启用
gzip
压缩 - 按需加载资源文件
require.ensure
- 优化
devtool
中的source-map
- 剥离
css
文件,独自打包 - 去除不必要插件,通常就是开发环境与生产环境用同一套配置文件导致
Tree Shaking
在构建打包过程中,移除那些引入但未被应用的有效代码-
开启
scope hosting
- 体积更小
- 创立函数作用域更小
- 代码可读性更好
New 操作符做了什么事件?
1、首先创立了一个新对象
2、设置原型,将对象的原型设置为函数的 prototype 对象
3、让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象增加属性)4、判断函数的返回值类型,如果是值类型,返回创立的对象。如果是援用类型,就返回这个援用类型的对象
二分查找 – 工夫复杂度 log2(n)
题目形容: 如何确定一个数在一个有序数组中的地位
实现代码如下:
function search(arr, target, start, end) {
let targetIndex = -1;
let mid = Math.floor((start + end) / 2);
if (arr[mid] === target) {
targetIndex = mid;
return targetIndex;
}
if (start >= end) {return targetIndex;}
if (arr[mid] < target) {return search(arr, target, mid + 1, end);
} else {return search(arr, target, start, mid - 1);
}
}
// const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// const position = search(dataArr, 6, 0, dataArr.length - 1);
// if (position !== -1) {// console.log(` 指标元素在数组中的地位:${position}`);
// } else {// console.log("指标元素不在数组中");
// }
什么是同源策略
跨域问题其实就是浏览器的同源策略造成的。
同源策略限度了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在歹意文件的重要的平安机制。同源指的是:协定 、 端口号 、 域名 必须统一。
同源策略:protocol(协定)、domain(域名)、port(端口)三者必须统一。
同源政策次要限度了三个方面:
- 以后域下的 js 脚本不可能拜访其余域下的 cookie、localStorage 和 indexDB。
- 以后域下的 js 脚本不可能操作拜访操作其余域下的 DOM。
- 以后域下 ajax 无奈发送跨域申请。
同源政策的目标次要是为了保障用户的信息安全,它只是对 js 脚本的一种限度,并不是对浏览器的限度,对于个别的 img、或者 script 脚本申请都不会有跨域的限度,这是因为这些操作都不会通过响应后果来进行可能呈现平安问题的操作。
懒加载的特点
- 缩小无用资源的加载:应用懒加载显著缩小了服务器的压力和流量,同时也减小了浏览器的累赘。
- 晋升用户体验: 如果同时加载较多图片,可能须要期待的工夫较长,这样影响了用户体验,而应用懒加载就能大大的进步用户体验。
- 避免加载过多图片而影响其余资源文件的加载:会影响网站利用的失常应用。
浏览器的次要组成部分
- ⽤户界⾯ 包含地址栏、后退 / 后退按钮、书签菜单等。除了浏览器主窗⼝显示的您申请的⻚⾯外,其余显示的各个局部都属于⽤户界⾯。
- 浏览器引擎 在⽤户界⾯和出现引擎之间传送指令。
- 出现引擎 负责显示申请的内容。如果申请的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- ⽹络 ⽤于⽹络调⽤,⽐如 HTTP 申请。其接⼝与平台⽆关,并为所有平台提供底层实现。
- ⽤户界⾯后端 ⽤于绘制根本的窗⼝⼩部件,⽐如组合框和窗⼝。其公开了与平台⽆关的通⽤接⼝,⽽在底层使⽤操作系统的⽤户界⾯⽅法。
- JavaScript 解释器。⽤于解析和执⾏ JavaScript 代码。
- 数据存储 这是长久层。浏览器须要在硬盘上保留各种数据,例如 Cookie。新的 HTML 标准 (HTML5) 定义了“⽹络数据库”,这是⼀个残缺(然而轻便)的浏览器内数据库。
值得注意的是,和⼤少数浏览器不同,Chrome 浏览器的每个标签⻚都别离对应⼀个出现引擎实例。每个标签⻚都是⼀个独⽴的过程。
参考 前端进阶面试题具体解答
手写题:数组扁平化
function flatten(arr) {let result = [];
for (let i = 0; i < arr.length; i++) {if (Array.isArray(arr[i])) {result = result.concat(flatten(arr[i]));
} else {result = result.concat(arr[i]);
}
}
return result;
}
const a = [1, [2, [3, 4]]];
console.log(flatten(a));
对浏览器内核的了解
浏览器内核次要分成两局部:
- 渲染引擎的职责就是渲染,即在浏览器窗口中显示所申请的内容。默认状况下,渲染引擎能够显示 html、xml 文档及图片,它也能够借助插件显示其余类型数据,例如应用 PDF 阅读器插件,能够显示 PDF 格局。
- JS 引擎:解析和执行 javascript 来实现网页的动态效果。
最开始渲染引擎和 JS 引擎并没有辨别的很明确,起初 JS 引擎越来越独立,内核就偏向于只指渲染引擎。
如何阻止事件冒泡
- 一般浏览器应用:event.stopPropagation()
- IE 浏览器应用:event.cancelBubble = true;
PWA 应用过吗?serviceWorker 的应用原理是啥?
渐进式网络应用(PWA)
是谷歌在 2015 年底提出的概念。基本上算是 web 应用程序,但在外观和感觉上与 原生 app
相似。反对 PWA
的网站能够提供脱机工作、推送告诉和设施硬件拜访等性能。
Service Worker
是浏览器在后盾独立于网页运行的脚本,它关上了通向不须要网页或用户交互的性能的大门。当初,它们已包含如推送告诉和后盾同步等性能。未来,Service Worker
将会反对如定期同步或天文围栏等其余性能。本教程探讨的外围性能是拦挡和解决网络申请,包含通过程序来治理缓存中的响应。
图片懒加载
与一般的图片懒加载不同,如下这个多做了 2 个精心解决:
- 图片全副加载实现后移除事件监听;
- 加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length
// 修改谬误,须要加上自执行
- const imgLazyLoad = function() {+ const imgLazyLoad = (function() {
let count = 0
return function() {let deleteIndexList = []
imgList.forEach((img, index) => {let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
deleteIndexList.push(index)
count++
if (count === length) {document.removeEventListener('scroll', imgLazyLoad)
}
}
})
imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
}
- }
+ })()
// 这里最好加上防抖解决
document.addEventListener('scroll', imgLazyLoad)
浏览器渲染优化
(1)针对 JavaScript: JavaScript 既会阻塞 HTML 的解析,也会阻塞 CSS 的解析。因而咱们能够对 JavaScript 的加载形式进行扭转,来进行优化:
(1)尽量将 JavaScript 文件放在 body 的最初
(2)body 两头尽量不要写 <script>
标签
(3)<script>
标签的引入资源形式有三种,有一种就是咱们罕用的间接引入,还有两种就是应用 async 属性和 defer 属性来异步引入,两者都是去异步加载内部的 JS 文件,不会阻塞 DOM 的解析(尽量应用异步加载)。三者的区别如下:
- script 立刻进行页面渲染去加载资源文件,当资源加载结束后立刻执行 js 代码,js 代码执行结束后持续渲染页面;
- async 是在下载实现之后,立刻异步加载,加载好后立刻执行,多个带 async 属性的标签,不能保障加载的程序;
- defer 是在下载实现之后,立刻异步加载。加载好后,如果 DOM 树还没构建好,则先等 DOM 树解析好再执行;如果 DOM 树曾经筹备好,则立刻执行。多个带 defer 属性的标签,依照程序执行。
(2)针对 CSS:应用 CSS 有三种形式:应用link、@import、内联款式,其中 link 和 @import 都是导入内部款式。它们之间的区别:
- link:浏览器会派发一个新等线程 (HTTP 线程) 去加载资源文件,与此同时 GUI 渲染线程会持续向下渲染代码
- @import:GUI 渲染线程会临时进行渲染,去服务器加载资源文件,资源文件没有返回之前不会持续渲染(妨碍浏览器渲染)
- style:GUI 间接渲染
内部款式如果长时间没有加载结束,浏览器为了用户体验,会应用浏览器会默认款式,确保首次渲染的速度。所以 CSS 个别写在 headr 中,让浏览器尽快发送申请去获取 css 款式。
所以,在开发过程中,导入内部款式应用 link,而不必 @import。如果 css 少,尽可能采纳内嵌款式,间接写在 style 标签中。
(3)针对 DOM 树、CSSOM 树: 能够通过以下几种形式来缩小渲染的工夫:
- HTML 文件的代码层级尽量不要太深
- 应用语义化的标签,来防止不规范语义化的非凡解决
- 缩小 CSSD 代码的层级,因为选择器是从左向右进行解析的
(4)缩小回流与重绘:
- 操作 DOM 时,尽量在低层级的 DOM 节点进行操作
- 不要应用
table
布局,一个小的改变可能会使整个table
进行从新布局 - 应用 CSS 的表达式
- 不要频繁操作元素的款式,对于动态页面,能够批改类名,而不是款式。
- 应用 absolute 或者 fixed,使元素脱离文档流,这样他们发生变化就不会影响其余元素
- 防止频繁操作 DOM,能够创立一个文档片段
documentFragment
,在它下面利用所有 DOM 操作,最初再把它增加到文档中 - 将元素先设置
display: none
,操作完结后再把它显示进去。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。 - 将 DOM 的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于 浏览器的渲染队列机制。
浏览器针对页面的回流与重绘,进行了本身的优化——渲染队列
浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了肯定的数量或者到了肯定的工夫距离,浏览器就会对队列进行批处理。这样就会让屡次的回流、重绘变成一次回流重绘。
将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,本来应该是触发屡次回流,变成了只触发一次回流。
寄生组合继承
题目形容: 实现一个你认为不错的 js 继承形式
实现代码如下:
function Parent(name) {
this.name = name;
this.say = () => {console.log(111);
};
}
Parent.prototype.play = () => {console.log(222);
};
function Children(name) {Parent.call(this);
this.name = name;
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;
// let child = new Children("111");
// // console.log(child.name);
// // child.say();
// // child.play();
ES6 之前应用 prototype 实现继承
Object.create() 会创立一个“新”对象,而后将此对象外部的 [[Prototype]] 关联到你指定的对象(Foo.prototype)。Object.create(null) 创立一个空 [[Prototype]] 链接的对象,这个对象无奈进行委托。
function Foo(name) {this.name = name;}
Foo.prototype.myName = function () {return this.name;}
// 继承属性,通过借用结构函数调用
function Bar(name, label) {Foo.call(this, name);
this.label = label;
}
// 继承办法,创立备份
Bar.prototype = Object.create(Foo.prototype);
// 必须设置回正确的构造函数,要不然在会产生判断类型出错
Bar.prototype.constructor = Bar;
// 必须在上一步之后
Bar.prototype.myLabel = function () {return this.label;}
var a = new Bar("a", "obj a");
a.myName(); // "a"
a.myLabel(); // "obj a"
变量晋升
函数在运行的时候,会首先创立执行上下文,而后将执行上下文入栈,而后当此执行上下文处于栈顶时,开始运行执行上下文。
在创立执行上下文的过程中会做三件事:创立变量对象,创立作用域链,确定 this 指向,其中创立变量对象的过程中,首先会为 arguments 创立一个属性,值为 arguments,而后会扫码 function 函数申明,创立一个同名属性,值为函数的援用,接着会扫码 var 变量申明,创立一个同名属性,值为 undefined,这就是变量晋升。
Nginx 的概念及其工作原理
Nginx 是一款轻量级的 Web 服务器,也能够用于反向代理、负载平衡和 HTTP 缓存等。Nginx 应用异步事件驱动的办法来解决申请,是一款面向性能设计的 HTTP 服务器。
传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于 event-driven 模型的。正是这个次要的区别带给了 Nginx 在性能上的劣势。
Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其余的 worker process,这一点和 Apache 十分像,然而 Nginx 的 worker process 能够同时解决大量的 HTTP 申请,而每个 Apache process 只能解决一个。
箭头函数和一般函数有啥区别?箭头函数能当构造函数吗?
- 一般函数通过 function 关键字定义,this 无奈联合词法作用域应用,在运行时绑定,只取决于函数的调用形式,在哪里被调用,调用地位。(取决于调用者,和是否独立运行)
-
箭头函数应用被称为“胖箭头”的操作
=>
定义,箭头函数不利用一般函数 this 绑定的四种规定,而是依据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无奈被批改(new 也不行)。- 箭头函数罕用于回调函数中,包含事件处理器或定时器
- 箭头函数和 var self = this,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域
- 没有原型、没有 this、没有 super,没有 arguments,没有 new.target
-
不能通过 new 关键字调用
- 一个函数外部有两个办法:[[Call]] 和 [[Construct]],在通过 new 进行函数调用时,会执行 [[construct]] 办法,创立一个实例对象,而后再执行这个函数体,将函数的 this 绑定在这个实例对象上
- 当间接调用时,执行 [[Call]] 办法,间接执行函数体
- 箭头函数没有 [[Construct]] 办法,不能被用作结构函数调用,当应用 new 进行函数调用时会报错。
function foo() {return (a) => {console.log(this.a);
}
}
var obj1 = {a: 2}
var obj2 = {a: 3}
var bar = foo.call(obj1);
bar.call(obj2);
同步和异步的区别
- 同步 指的是当一个过程在执行某个申请时,如果这个申请须要期待一段时间能力返回,那么这个过程会始终期待上来,直到音讯返回为止再持续向下执行。
- 异步 指的是当一个过程在执行某个申请时,如果这个申请须要期待一段时间能力返回,这个时候过程会持续往下执行,不会阻塞期待音讯的返回,当音讯返回时零碎再告诉过程进行解决。
深拷贝(思考到复制 Symbol 类型)
题目形容: 手写 new 操作符实现
实现代码如下:
function isObject(val) {return typeof val === "object" && val !== null;}
function deepClone(obj, hash = new WeakMap()) {if (!isObject(obj)) return obj;
if (hash.has(obj)) {return hash.get(obj);
}
let target = Array.isArray(obj) ? [] : {};
hash.set(obj, target);
Reflect.ownKeys(obj).forEach((item) => {if (isObject(obj[item])) {target[item] = deepClone(obj[item], hash);
} else {target[item] = obj[item];
}
});
return target;
}
// var obj1 = {
// a:1,
// b:{a:2}
// };
// var obj2 = deepClone(obj1);
// console.log(obj1);
实现节流函数和防抖函数
函数防抖的实现:
function debounce(fn, wait) {
var timer = null;
return function() {
var context = this,
args = [...arguments];
// 如果此时存在定时器的话,则勾销之前的定时器从新记时
if (timer) {clearTimeout(timer);
timer = null;
}
// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {fn.apply(context, args);
}, wait);
};
}
函数节流的实现:
// 工夫戳版
function throttle(fn, delay) {var preTime = Date.now();
return function() {
var context = this,
args = [...arguments],
nowTime = Date.now();
// 如果两次工夫距离超过了指定工夫,则执行函数。if (nowTime - preTime >= delay) {preTime = Date.now();
return fn.apply(context, args);
}
};
}
// 定时器版
function throttle (fun, wait){
let timeout = null
return function(){
let context = this
let args = [...arguments]
if(!timeout){timeout = setTimeout(() => {fun.apply(context, args)
timeout = null
}, wait)
}
}
}