此文主要描述 html / css / js / react 即时渲染和网络加载优化方面的知识,webpack常用优化方法和HTTP Server等优化请关注《 前端性能优化指南(2) 》如果之后发现有其它要点值得梳理,会继续更新本文...
目录
》 思维导图:
前端性能优化是个很大的概念,涉及HTTP协议、浏览器渲染原理、操作系统和网络、前端工程化和Js底层原理等各个方面。通过建立思维导图可以让我们很好的将各个优化方面组织和联系起来。
按照优化原理的不同则可以将其分为网络层面优化
和渲染层面
的优化,网络层面的优化更多体现在资源加载时的优化,而渲染层的优化更多体现在运行时优化。
例如优化浏览器缓存策略以减少HTTP请求传输量、图片和其它静态资源的压缩、服务器端启用Gzip压缩、使用CDN、图片懒加载、延迟脚本Defer和异步脚本Async等属于网络层面的优化。另一方面,减少页面的回流和重绘、使用React.Fragment减少界面dom层级、使用骨架屏、函数节流和去抖、React长列表组件优化、通过事件冒泡机制实现事件委托等就属于渲染层面的优化。
➣ HTML/CSS 性能优化方面
1. 网络层面
1)抽离内联样式内联脚本
- 内联资源不利于浏览器缓存,造成重复的资源请求
- 内联资源会造成HTML臃肿,不利于HTTP传输
- 内联资源的下载和解析可能会阻塞导致界面渲染,导致界面白屏
- 内联资源不好管理和维护
2)defer脚本和async脚本
HTML在解析时遇到声明的脚本会立即下载和执行,往往会延迟界面剩余部分的解析,造成界面白屏的情况。比较古老的优化方式之一就是将脚本放到HTML文档末尾,这样子解决了白屏的问题,可是在DOM文档结构复杂冗长时,也会造成一定的界面脚本下载和执行延迟,script标签新属性
async
和defer
可以解决此类问题:
- defer脚本
延迟脚本-声明defer
属性的外部脚本下载时不会阻塞HTML的解析和渲染,并且会在HTML渲染完成并且可实际操作之后开始执行(
DOMContentLoaded
事件被触发之前),各个脚本解析执行顺序对应声明时的位置顺序,执行完成后会触发页面DOMContentLoaded
事件。
- async脚本
异步脚本-声明async
属性的外部脚本下载时不会阻塞HTML的解析和渲染,各个脚本的下载和执行完全独立,下载完成后即开始执行,所以执行顺序不固定,与
DOMContentLoaded
事件的触发没有关联性。
- 动态脚本加载技术
在脚本执行时动态运行loadScript
函数可以实现类似延迟脚本和异步脚本的效果:isDefer
为真值时脚本的执行顺序为脚本位置顺序,为假值时效果同于异步脚本。
function loadScript(src, isDefer) {
let script = document.createElement('script');
script.src = src;
script.async = !isDefer;
document.body.append(script);
}
3)压缩HTML/CSS代码资源
代码资源中存在很多无用的空格和符号等,去除他们带来的效益是可观的,另一方面压缩资源也能起到源代码保护的作用。现代前端工程化框架一般继承了此类压缩插件,比如webpack框架的uglifyjs
插件。
4)压缩图片/音视频等多媒体资源
其实网页带宽往往被图片等资源大量占用,压缩他们能带来超出预期的优化效益。现代前端工程化框架一般继承了此类压缩插件,如imagemin-webpack-plugin
插件。
5)使用雪碧图
使用雪碧图本质上优化了HTTP请求的数量,将众多图片拼贴为一张作为背景图片引用,然后我们给一个元素设置固定大小,让它的背景图片位置进行变化,就好像显示出了不同的图片,这就是雪碧图的原理。
6)避免空的 src 和 href 值
当link标签的href属性为空、script标签的src属性为空的时候,浏览器渲染的时候会把当前页面的URL作为它们的属性值,从而把页面的内容加载进来作为它们的值。
7)避免使用@import
来引入css
这种语法会阻止多个css文件的并行下载,被@import
引入的css文件会在引入它的css文件下载并渲染好之后才开始下载渲染自身。并且@import
引入的css文件的下载顺序会被打乱,排列在@import
之后的JS文件会先于@import
下载。
2. 渲染层面
1)减少页面的回流和重绘
- 使用CSS3属性
transform
来实现元素位移 - 让动画效果应用到
position: fixed/absolute
的元素上,原理是让其脱离文档流 - 向界面插入大量dom节点时先将dom元素添加到虚拟dom操作节点
DocumentFragment
上,最后再将虚拟节点实际添加到界面上。 - 避免直接使用JS操作dom元素的style样式,可以使用class一次性改变dom样式类。
- 将会引起页面回流、重绘的操作尽量放到DOM树的后面,减少级联反应。
- 使用CSS3动画Animation来实现一些复杂元素的动画效果,原理是利用了硬件加速
读取一些容易引起回流的元素属性注意使用变量缓存
elem.offsetLeft, elem.offsetTop, elem.offsetWidth, elem.offsetHeight, elem.offsetParent elem.clientLeft, elem.clientTop, elem.clientWidth, elem.clientHeight elem.getClientRects(), elem.getBoundingClientRect() elem.scrollBy(), elem.scrollTo() elem.scrollIntoView(), elem.scrollIntoViewIfNeeded() elem.scrollWidth, elem.scrollHeight elem.scrollLeft, elem.scrollTop 除了读取,设置也会触发 ...
2)减少DOM结构的层级
3)尽量不使用table
布局和iframe
内联网页
/* table布局 */
table布局不灵活,不利于css样式定制
table布局渲染性能较低,可能触发多次重绘
table布局不利于html语义化
/* iframe */
iframe会阻塞主页面的onload事件
iframe和主页面共享HTTP连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载
iframe不利于网页布局
iframe对移动端不友好
iframe的反复重新加载可能导致一些浏览器的内存泄露
iframe中的数据传输复杂
iframe不利于SEO
4)CSS选择器的使用策略
浏览器是从选择器的右边到左边读取,选择器最右边的部分被称为关键选择器,与CSS选择器规则的效率相关。
效率排序如下:
内联样式 > ID 选择器 > 类选择器 = 属性选择器 = 伪类选择器 > 标签选择器 = 伪元素选择器
要点:
- 关键选择器避免使用通用选择器*,其查询开销较大
- 使用ID/Class选择器时尽量使其独立,因为无用的上层规则(标签、类名)只会增加查找时间,ID/Class已经具有单独筛选元素的能力
- 避免使用子选择器,尤其是将其与标签、通配符组合使用,性能开销较大
- 利用CSS元素属性继承的特性,是多个元素复用多一种规则
- 移除无匹配样式,否则会造成无用的样式解析和匹配,同时增大CSS文件体积
5)flex布局的性能比inline-block
和float
布局都要好
6)css的书写顺序也会对其解析渲染性能造成影响
浏览器从上到下开始解析一段css规则,将容易造成回流、重绘的属性放在上部可以让渲染引擎更高效地工作,可以按照下列顺序来进行书写,使用编辑器的csslint
插件可以辅助完成这一过程:
定位属性
position display float left top right bottom overflow clear z-index
几何属性
width height padding border margin background
文字样式
font-family font-size font-style font-weight font-varient color
文本属性
text-align vertical-align text-wrap text-transform text-indent text-decoration letter-spacing word-spacing white-space text-overflow
CSS3中新增属性
content box-shadow border-radius transform
➣ Javascript 性能优化方面
1. 网络层面
1)压缩JS代码资源
代码资源中存在很多无用的空格和符号等,去除他们带来的效益是可观的,另一方面压缩资源也能起到源代码保护的作用。现代前端工程化框架一般继承了此类压缩插件,比如webpack框架的uglifyjs
插件。
2. 渲染层面
1)使用函数节流和函数去抖处理一些函数的高频触发调用
在面对一些需要进行调用控制的函数高频触发场景时,可能有人会对何时使用节流何时使用去抖产生疑问。这里通过一个特性进行简单区分:如果你需要保留短时间内高频触发的最后一次结果时,那么使用去抖函数,如果你需要对函数的调用次数进行限制,以最佳的调用间隔时间保持函数的持续调用而不关心是否是最后一次调用结果时,请使用节流函数。
比如echarts图常常需要在窗口resize之后重新使用数据渲染,但是直接监听resize事件可能导致短时间内渲染函数被触发多次。我们可以使用函数去抖的思想,监听resize事件后在监听器函数里获取参数再使用参数调用事先初始化好了的throttle函数,保证resize过程结束后能触发一次实际的echarts重渲染即可。
节流
throttle
function throttle(fn, time) { let canRun = true; return function() { if (canRun) { canRun = false; setTimeout(() => { fn.apply(this, arguments); canRun = true; }, time); } }; }
去抖
debounce
function debounce(fn, time) { let timer; return function() { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, arguments); }, time); }; }
2)Js实现动画时使用requestAnimationFrame
替代定时器
window.requestAnimationFrame()
告诉浏览器你希望执行一个动画,并且要求浏览器在下次重绘之前(每帧之前)调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
设置的回调函数在被调用时会被传入触发的时间戳,在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间,我们可以记录前后时间戳差值来控制元素动画的速度和启停。
如果换用过定时器setTimeout/setInterval
来控制帧动画的话,一般我们采用60帧进行动画绘制,所以设置的定时时间就应该是1000 / 60 = 17ms
。不过由于定时器本身只是把回调函数放入了宏任务队列
,其精确度受到主进程代码执行栈影响,可能导致帧动画的回调函数在浏览器的一次渲染过程中才被触发(理想情况是渲染前调用回调函数获得计算值,渲染时执行计算值绘制),因此本应在当前帧呈现的绘制效果被延迟到了下一帧,产生丢帧卡顿的情况。
这里让我们使用requestAnimationFrame
来实现一个动画处理类作为例子,使用方式如下:
var anime = new Animation();
anime.setTarget('#animationTarget');
// 右下角移动50px
anime.push('#animationTarget', { x: 50, y: 50, duration: 1000, func: 'easeIn' });
// 右上角移动50px
anime.push('#animationTarget', { x: -50, y: -50, duration: 500, func: 'linear' });
预览图:
/**
* [tween 缓动算法]
* @param {[Number]} time [动画已经消耗的时间]
* @param {[String]} start [目标开始的位置]
* @param {[String]} distance [目标开始位置和结束位置的距离]
* @param {[Number]} duration [动画总持续时间]
*/
var tween = {
linear: function( time, start, distance, duration ) { return distance*time/duration + start; },
easeIn: function( time, start, distance, duration ) { return distance * ( time /= duration ) * time + start; },
strongEaseIn: function(time, start, distance, duration) { return distance * ( time /= duration ) * time * time * time * time + start; },
strongEaseOut: function(time, start, distance, duration) { return distance * ( ( time = time / duration - 1) * time * time * time * time + 1 ) + start; },
sinEaseIn: function( time, start, distance, duration ) { return distance * ( time /= duration) * time * time + start; },
sinEaseOut: function(time,start,distance,duration){ return distance * ( ( time = time / duration - 1) * time * time + 1 ) + start; },
};
/* ------------------- 动画控制类 ------------------- */
function Animation() {
this.store = {};
};
/* ------------------- 初始化处理元素 ------------------- */
Animation.prototype.setTarget = function (selector) {
var element = document.querySelector(selector);
if (element) {
// element.style.position = 'relative';
this.store[selector] = {
selector: selector,
element: document.querySelector(selector),
status: 'pending',
queue: [],
timeStart: '',
positionStart: { x: '', y: '' },
positionEnd: { x: '', y: '' },
};
}
};
/**
* [start 开始动画]
* @param {[String]} selector [选择器]
* @param {[type]} func [缓动动画]
*/
Animation.prototype.start = function (selector, func) {
var that = this;
var target = this.store[selector];
target.status = 'running';
// 帧调用函数
that.update({x: 0, y: 0}, selector);
};
/**
* [update 更新位置]
* @param {[type]} selector [description]
*/
Animation.prototype.update = function (position, selector) {
var target = this.store[selector],
that = this,
timeUsed,
positionX, positionY;
//
if (!target || !target.queue.length) {
target.status = 'pending';
return;
};
// reset position
target.element.style.left = position.x + 'px';
target.element.style.top = position.y + 'px';
// position
target.positionStart = { x: position.x, y: position.y };
target.positionEnd = { x: position.x + target.queue[0].x, y: position.y + target.queue[0].y };
// time
target.timeStart = null;
// 递归调用
var callback = function (time) {
if (target.timeStart === null) target.timeStart = time; // 动画开始时间
timeUsed = time - target.timeStart;
// 当前动画完成
if (timeUsed >= target.queue[0].duration) {
target.queue.shift();
that.step(target.element, target.positionEnd.x, target.positionEnd.y);
target.status = 'running';
// var position = target.element.getBoundingClientRect();
var position = {
x: parseInt(target.element.style.left),
y: parseInt(target.element.style.top),
};
// 下一个动画
that.update(position, selector);
return;
}
positionX = target.queue[0].func(
timeUsed,
target.positionStart.x,
target.positionEnd.x - target.positionStart.x,
target.queue[0].duration,
);
positionY = target.queue[0].func(
timeUsed,
target.positionStart.y,
target.positionEnd.y - target.positionStart.y,
target.queue[0].duration,
);
that.step(target.element, positionX, positionY);
requestAnimationFrame(callback);
};
requestAnimationFrame(callback);
};
/**
* [step dom操作]
* @param {[DOM]} element [dom 元素]
* @param {[Number]} x [x坐标]
* @param {[Number]} y [y坐标]
*/
Animation.prototype.step = function (element, x, y) {
element.style.left = x + 'px';
element.style.top = y + 'px';
};
/**
* [push 加入动画队列]
* @param {[String]} selector [dom选择器]
* @param {[Object]} conf [位置数据]
*/
Animation.prototype.push = function (selector, conf) {
if (this.store[selector]) {
this.store[selector].queue.push({
x: conf.x,
y: conf.y,
duration: conf.duration || 1000,
func: tween[conf.func] || tween['linear'],
});
}
};
/* ------------------- 动画出队列 ------------------- */
Animation.prototype.pop = function (selector) {
if (this.store[selector]) {
this.store[selector].queue.pop();
}
};
/* ------------------- 清空动画队列 ------------------- */
Animation.prototype.clear = function (selector) {
if (this.store[selector]) {
this.store[selector].queue.length = 1;
}
};
3)使用IntersectionObserver
API来替代scroll
事件实现元素相交检测
以下是一些需要用到相交检测的场景:
- 图片懒加载 -- 当图片滚动到可见时才进行加载
- 内容无限滚动 -- 用户滚动到接近滚动容器底部时直接加载更多数据,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
- 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
- 在用户看见某个区域时执行任务、播放视频
以内容无限滚动为例,古老的相交检测方案就是使用scroll
事件监听滚动容器,在监听器函数中获取滚动元素的几何属性判断元素是否已经滚动到底部。我们知道scrollTop
等属性的获取和设置都会导致页面回流,并且如果界面需要绑定多个监听函数到scroll
事件进行类似操作的时候,页面性能会大打折扣:
/* 滚动监听 */
onScroll = () => {
const {
scrollTop, scrollHeight, clientHeight
} = document.querySelector('#target');
/* 已经滚动到底部 */
// scrollTop(向上滚动的高度);clientHeight(容器可视总高度);scrollHeight(容器的总内容长度)
if (scrollTop + clientHeight === scrollHeight) { /* do something ... */ }
}
因此在处理相交检测的问题时我们应该在考虑兼容性的情况下尽可能使用IntersectionObserver
API,浏览器会自行优化多个元素的相交管理。IntersectionObserver API 允许你配置一个回调函数,当以下情况发生时会被调用:
- 每当目标(target)元素与设备视窗或者其他指定元素发生交集的时候执行。设备视窗或者其他元素我们称它为根元素或根(root)。
- Observer第一次监听目标元素的时候
创建一个 IntersectionObserver对象,并传入相应参数和回调用函数,该回调函数将会在目标(target)元素和根(root)元素的交集大小超过阈值(threshold)规定的大小时候被执行:
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px', // 指定根(root)元素的外边距
threshold: 1.0, // 表示子元素完全和容器元素相交
}
const observer = new IntersectionObserver(callback, options);
observer.observe(document.querySelector('#scrollTarget'));
配置项1: 通常需要关注文档最接近的可滚动祖先元素的交集更改,如果元素不是可滚动元素的后代,则默认为设备视窗。如果要观察相对于根(root)元素的交集,请指定根(root)元素为null。
配置项2: 目标(target)元素与根(root)元素之间的交叉度是交叉比(intersection ratio)。这是目标(target)元素相对于根(root)的交集百分比的表示,它的取值在0.0和1.0之间。
配置项3: 根(root)元素的外边距。类似于 CSS 中的 margin 属性,比如 "10px 20px 30px 40px" (top, right, bottom, left)。如果有指定root参数,则rootMargin也可以使用百分比来取值。该属性值是用作root元素和target发生交集时候的计算交集的区域范围,使用该属性可以控制root元素每一边的收缩或者扩张。默认值为0。
这里我们再以一个实际案例来进行展示,即图片懒加载方案:
(function lazyload() {
var imagesToLoad = document.querySelectorAll('image[data-src]');
function loadImage(image) {
image.src = image.getAttribute('data-src');
image.addEventListener('load', function() {
image.removeAttribute('data-src');
});
}
var intersectionObserver = new IntersectionObserver(function(items, observer) {
items.forEach(function(item) {
/* 所有属性:
item.boundingClientRect - 目标元素的几何边界信息
item.intersectionRatio - 相交比 intersectionRect/boundingClientRect
item.intersectionRect - 描述根和目标元素的相交区域
item.isIntersecting - true(相交开始),false(相交结束)
item.rootBounds - 描述根元素
item.target - 目标元素
item.time - 时间原点(网页在窗口加载完成时的时间点)到交叉被触发的时间的时间戳
*/
if (item.isIntersecting) {
loadImage(item.target);
observer.unobserve(item.target);
}
});
});
imagesToLoad.forEach(function(image) {
intersectionObserver.observe(image);
});
})();
4)使用Web-Workers
在后台运行CPU密集型任务
Web Worker 允许你在后台线程中运行脚本。如果你有一些高强度的任务,可以将它们分配给 Web Worker,这些 WebWorker 可以在不干扰用户界面的情况下运行它们。创建后,Web Worker 可以将消息发布到该代码指定的事件处理程序来与 JavaScript 代码通信,反之亦然。
一个简单的专用worker示例,我们在主进程代码中创建一个worker实例,然后向实例发送一个数字,worker接受到消息后拿到数字进行一次斐波那契函数
运算,并发送运算结果给主线程:
/* -------------- main.js -------------- */
var myWorker = new Worker("fibonacci.js");
worker.onmessage = function (e) {
console.log('The result of fibonacci.js: ', e.data);
};
worker.postMessage(100);
/* -------------- fibonacci.js -------------- */
function fibonacci(n) {
if (n > 1)
return fibonacci(n - 2) + fibonacci(n - 1);
return n;
}
self.onmessage = function(e) {
self.postMessage(fibonacci(Number(e.data)));
}
Worker的常见类型
- 专用Worker: 一个专用worker仅仅能被生成它的脚本所使用。
- 共享Worker: 一个共享worker可以被多个脚本使用——即使这些脚本正在被不同的window、iframe或者worker访问。
- Service Workers: 一般作为web应用程序、浏览器和网络(如果可用)之前的代理服务器。它们旨在(除开其他方面)创建有效的离线体验,拦截网络请求,以及根据网络是否可用采取合适的行动并更新驻留在服务器上的资源。他们还将允许访问推送通知和后台同步API。
- Chrome Workers: 一种仅适用于firefox的worker。如果您正在开发附加组件,希望在扩展程序中使用worker且有在你的worker中访问 js-ctypes 的权限,你可以使用Chrome Workers。
- Audio Workers: 音频worker使得在web worker上下文中直接完成脚本化音频处理成为可能。
Worker中可以使用的函数和接口
你可以在web worker中使用大多数的标准javascript特性,包括:- Navigator
- Location(只读)
- XMLHttpRequest
- Array, Date, Math, and String
- setTimeout/setInterval
- Cache & IndexedDB
- 关于线程安全
Worker接口会生成真正的操作系统级别的线程,然而,对于 web worker 来说,与其他线程的通信点会被很小心的控制,这意味着你很难引起并发问题。你没有办法去访问非线程安全的组件或者是 DOM,此外你还需要通过序列化对象来与线程交互特定的数据。所以你要是不费点劲儿,还真搞不出错误来。
- 内容安全策略
有别于创建它的document对象,worker有它自己的执行上下文。因此普遍来说,worker并不受限于创建它的document(或者父级worker)的内容安全策略。举个例子,假设一个document有如下头部声明:Content-Security-Policy: script-src 'self'
,这个声明有一部分作用在于禁止脚本代码使用eval()方法。然而,如果脚本代码创建了一个worker,在worker中却是可以使用eval()的。
为了给worker指定内容安全策略,必须为发送worker代码的请求本身加上一个内容安全策略
。有一个例外情况,即worker脚本的使用dataURL或者blob创建的话,worker会继承创建它的document或者worker的内容安全策略。
一些使用场景
- 在一些不采用
websockets
架构的应用中使用传统的轮询方式定时获取接口数据以供前端脚本实现一些界面和数据自动更新功能 - 光线追踪:光线追踪是一种通过将光线追踪为像素来生成图像的渲染技术。光线追踪使用CPU密集型数学计算来模拟光线路径。这个想法是模拟反射,折射,材质等一些效果。所有这些计算逻辑都可以添加到Web Worker中以避免阻塞UI线程。
- 加密:由于对个人和敏感数据的监管日益严格,端到端加密越来越受欢迎。加密可能是一件非常耗时的事情,特别是如果有很多数据必须经常加密(例如在将数据发送到服务器之前)。这是一个非常好的场景,可以使用Web Worker。
- 预取数据:为了优化您的网站或Web应用程序并缩短数据加载时间,您可以利用Web Workers预先加载和存储一些数据,以便稍后在需要时使用它。
- PWA进式Web应用程序:这种应用程序中即使网络连接不稳定,它们也必须快速加载。这意味着数据必须存储在本地浏览器中,这是IndexDB或类似的API进场的地方。为了在不阻塞UI线程的情况下使用,工作必须在Web Workers中完成。
- 在一些不采用
5)使用事件委托
事件委托就是把一个元素响应事件(click、keydown......)的函数委托到另一个元素。一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。=> 一篇不错的参考文章
其实我们熟悉的 React 框架也并不是将 click 事件直接绑定在 dom 上面,而是采用事件冒泡的形式冒泡到 document 上面,这个思路借鉴了事件委托机制。而更老一点的jQuery也是允许我们直接使用它提供的API来进行事件委托:
$('.parent').on('click', 'a', function () {
console.log('click event on tag a');
}
》关于事件冒泡机制:
》事件模型的三个阶段:
- 捕获阶段:在事件冒泡的模型中,捕获阶段不会响应任何事件
- 目标阶段:目标阶段就是指事件响应到触发事件的最底层元素上
- 冒泡阶段:冒泡阶段就是事件的触发响应会从最底层目标一层层地向外到最外层(根节点),事件代理即是利用
件冒泡的机制把里层所需要响应的事件绑定到外层
》事件委托的优点:
- 减少内存消耗,提升性能
我们不需要再为每个列表元素都绑定一个事件,只需要将事件函数绑定到父级ul
组件:
- item 1
- item 2
- item 3
......
- item n
- 动态绑定事件
比如上述的例子中列表项就几个,我们给每个列表项都绑定了事件。在很多时候,我们需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件。
如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的。所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。
》使用Element.matches
API简单实现事件委托:
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
document.getElementById('list').addEventListener('click', function (e) {
// 兼容性处理
var event = e || window.event;
var target = event.target || event.srcElement;
if (target.matches('li.class-1')) {
console.log('the content is: ', target.innerHTML);
}
});
》事件委托的局限性:
- 比如 focus、blur 之类的事件本身没有事件冒泡机制,所以无法委托。
- mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的。
6)一些编码方面的优化建议
- 长列表数据的遍历使用
for
循环替代forEach
。
for循环能通过关键字break
实现循环中断,forEach首先性能不如for,其次在处理一些需要条件断开的循环时比较麻烦(可以包裹try catch,然后throw error断开)。如果是数组类型的数据遍历的话,也可以使用array.every(item => { if (...) return false; else do something; })
来实现条件断开。
- 尽量不要在全局作用域声明过多变量
全局变量存在于全局上下文,全局上下文是作用域链的顶端,当通过作用域链进行变量查找的时候,会延长查找时间。全局执行上下文会一直存在于上下文执行栈,直到程序推出,这样会影响GC垃圾回收。如果局部作用域中定义了同名变量,会遮蔽或者污染全局。
可以使用单例模式来封装一系列逻辑(运用了闭包的原理),并通过一个公用的变量名暴露给作用域中的其它模块使用,同时也提高了代码的内聚性:
/* bad */
const workData = {};
function workA() { /* do something ... */ }
function workB() { /* do something ... */ }
function workC() { /* do something ... */ }
/* good */
const work = (function (initParams) {
const workData = {};
function workA() { /* do something ... */ }
function workB() { /* do something ... */ }
function workC() { /* do something ... */ }
return {
doWorkA: workA,
doWorkB: workB,
doWorkC: workC,
workSeries: function() {
this.doWorkB();
this.doWorkC();
}
};
})(initParams);
work.doWorkA();
work.workSeries();
- 使用
switch
和map
的方式处理需要大量逻辑判断的情况
连续的if
判断中在到达目标条件之前需要经过多个条件判断,而map和switch方式都能够通过条件直接找到对应的处理逻辑。
/* bad */
if (condition === 'a')
// do something
else if (condition === 'b')
// do something
else
...
/* good */
switch (condition) {
case 'a':
// do something ...
break;
case 'b':
// do something ...
break;
...
default:
break;
}
const conditionMap = {
a: function() { /* do something */ },
b: function() { /* do something */ },
...
};
conditionMap[condition]();
- 定义构造函数时使用原型声明对象的公用方法
我们在new
一个对象时,js所做的就是创建一个空对象,并把此对象作为构造函数的context来执行(参考call调用逻辑),执行后空对象上就被复制了构造函数的的属性和方法,然后js会把构造函数的原型绑定到对象的__proto__
属性上,最后构造函数将对象返回给我们使用。
从以上可以看出,如果我们直接把一些function逻辑写入构造函数的话,在对象创建的时候每个function都会在新对象上被创建一次,消耗额外的资源,且违反了程序复用原则。建议将function放入构造函数的原型,那么对象就能通过原型链查找来使用这个方法,而不是在对象自身上重新复制一个一模一样的逻辑。
/* bad */
function Structure(attr) {
this.attr = attr;
this.getAttr = (function() {
return this.attr;
}).bind(this);
}
var obj = new Structure('obj1');
obj.getAttr(); // from obj itself
/* good */
function Structure(attr) {
this.attr = attr;
}
Structure.prototype.getAttr = function() {
return this.attr;
}
var obj = new Structure('obj1');
obj.getAttr(); // from obj prototype chain
➣ React 性能优化方面
1. 网络层面
1)React jsx/js文件压缩
2)使用React.lazy
和React.Suspense
实现代码分割和懒加载
React开发的应用通常会借用webpack
这类项目打包器将编写的各个模块代码和引入的依赖库的代码打包成一个单独的JS文件,有些未做CSS样式分离优化的项目甚至连样式表都和JS文件打包在一起,然后在页面加载的HTML文件中需要下载了这一整个JS文件后之后才能进去到页面构建阶段。对于中小型项目还好,简单的首屏优化就能将资源压缩到足够小,但是一些大型项目可能存在很多子项目,如果不对代码做分割然后按子项目模块加载的话,在首屏我们浏览器需要下载整个项目的依赖文件,导致加载时间过长。
使用React.lazy
可以分割子项目代码并根据当前页面路由来动态加载页面依赖文件,尽管并没有减少应用整体的代码体积,但你可以避免加载用户永远不需要的代码,并在初始加载的时候减少所需加载的代码量。
注意:搭配Babel
进行代码编译时需要安装额外的babel插件以提供动态加载功能:
{
"presets": [...],
"plugins": [
"dynamic-import-webpack",
...
]
}
React.lazy 函数能让你像渲染常规组件一样处理动态引入的组件:
它接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。/* 使用前 */ import OtherComponent from './OtherComponent'; /* 使用后,代码将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包 */ const OtherComponent = React.lazy(() => import('./OtherComponent')); /* -------------- OtherComponent.js -------------- */ export default function() { return (other) };
使用 React.Suspense 提供一个组件加载时的占位组件:
import React, { Suspense } from 'react'; const OtherComponent = React.lazy(() => import('./OtherComponent')); function mainComponent() { return (
Loading...