我工作中用到的性能优化全面指南
在Web开发中,Web的性能优化是一个重要的话题。无论是页面加载速度,用户体验,或者是程序运行效率,都与Web的性能优化息息相关。
最小化和压缩代码
在构建过程中,为了减少文件的大小和加载时间,通常会对JavaScript代码进行最小化和压缩处理。这包括移除不必要的空格、换行、注释,以及缩短变量和函数名。工具如UglifyJS和Terser等可以帮助我们完成这个任务。
// 原始代码
function hello(name) {
let message = 'Hello, ' + name;
console.log(message);
}
// 压缩后的代码
function hello(n){var e='Hello, '+n;console.log(e)}
利用浏览器缓存
浏览器缓存是提升Web应用性能的一个重要手段。我们可以将一些经常用到的、变化不大的数据存储在本地,以减少对服务器的请求。例如,可以使用localStorage或sessionStorage来存储这些数据。
// 存储数据
localStorage.setItem('name', 'John');
// 获取数据
var name = localStorage.getItem('name');
// 移除数据
localStorage.removeItem('name');
// 清空所有数据
localStorage.clear();
避免过度使用全局变量
全局变量会占用更多的内存,并且容易导致命名冲突,从而降低程序的运行效率。我们应尽量减少全局变量的使用。
// 不好的写法
var name = 'John';
function greet() {
console.log('Hello, ' + name);
}
// 好的写法
function greet(name) {
console.log('Hello, ' + name);
}
greet('John');
使用事件委托减少事件处理器的数量
事件委托是将事件监听器添加到父元素,而不是每个子元素,以此来减少事件处理器的数量,并且提升性能。
document.getElementById('parent').addEventListener('click', function (event) {
if (event.target.classList.contains('child')) {
// 处理点击事件...
}
});
好的,下面我会详细解释一下这些概念以及相关的示例:
async 和 defer
async
和 defer
是用于控制 JavaScript 脚本加载和执行的 HTML 属性。
async
使浏览器在下载脚本的同时,继续解析 HTML。一旦脚本下载完毕,浏览器将中断 HTML 解析,执行脚本,然后继续解析 HTML。
<script async src="script.js"></script>
defer
也使浏览器在下载脚本的同时,继续解析 HTML。但是,脚本的执行会等到 HTML 解析完毕后再进行。
<script defer src="script.js"></script>
在需要控制脚本加载和执行的时机以优化性能的场景中,这两个属性是非常有用的。
防抖和节流
throttle
(节流)和 debounce
(防抖)。
throttle
保证函数在一定时间内只被执行一次。例如,一个常见的使用场景是滚动事件的监听函数:
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) return;
lastCall = now;
return func(...args);
};
}
window.addEventListener('scroll', throttle(() => console.log('Scrolling'), 100));
debounce
保证在一定时间内无新的触发后再执行函数。例如,实时搜索输入的监听函数:
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}
searchInput.addEventListener('input', debounce(() => console.log('Input'), 300));
利用虚拟DOM和Diff算法进行高效的DOM更新
当我们频繁地更新DOM时,可能会导致浏览器不断地进行重绘和回流,从而降低程序的性能。因此,我们可以使用虚拟DOM和Diff算法来进行高效的DOM更新。例如,React和Vue等框架就使用了这种技术。
// React示例
class Hello extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
ReactDOM.render(<Hello name="John" />, document.getElementById('root'));
避免长时间运行的任务
浏览器单线程的运行方式决定了JavaScript长时间运行的任务可能会阻塞UI渲染和用户交互,从而影响性能。对于这类任务,可以考虑将其分解为一系列较小的任务,并在空闲时执行,这就是“分片”
或者“时间切片”
的策略。
function chunk(taskList, iteration, context) {
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && taskList.length > 0) {
iteration.call(context, taskList.shift());
}
if (taskList.length > 0) {
chunk(taskList, iteration, context);
}
});
}
chunk(longTasks, (task) => {
task.execute();
}, this);
虚拟列表(Virtual List)
当我们在页面上渲染大量的元素时,这可能会导致明显的性能问题。虚拟列表是一种技术,可以通过只渲染当前可见的元素,来优化这种情况。
虚拟列表的等高方式实现:
// 列表项高度
const ITEM_HEIGHT = 20;
class VirtualList {
constructor(container, items, renderItem) {
this.container = container;
this.items = items;
this.renderItem = renderItem;
this.startIndex = 0;
this.endIndex = 0;
this.visibleItems = [];
this.update();
this.container.addEventListener('scroll', () => this.update());
}
update() {
const viewportHeight = this.container.clientHeight;
const scrollY = this.container.scrollTop;
this.startIndex = Math.floor(scrollY / ITEM_HEIGHT);
this.endIndex = Math.min(
this.startIndex + Math.ceil(viewportHeight / ITEM_HEIGHT),
this.items.length
);
this.render();
}
render() {
// 移除所有的可见元素
this.visibleItems.forEach((item) => this.container.removeChild(item));
this.visibleItems = [];
// 渲染新的可见元素
for (let i = this.startIndex; i < this.endIndex; i++) {
const item = this.renderItem(this.items[i]);
item.style.position = 'absolute';
item.style.top = `${i * ITEM_HEIGHT}px`;
this.visibleItems.push(item);
this.container.appendChild(item);
}
}
}
// 使用虚拟列表
new VirtualList(
document.getElementById('list'),
Array.from({ length: 10000 }, (_, i) => `Item ${i}`),
(item) => {
const div = document.createElement('div');
div.textContent = item;
return div;
}
);
、
优化循环
在处理大量数据时,循环的效率是非常重要的。我们可以通过一些方法来优化循环,例如:避免在循环中进行不必要的计算,使用倒序循环,使用forEach或map等函数。
// 不好的写法
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// 好的写法
let length = arr.length;
for (let i = 0; i < length; i++) {
console.log(arr[i]);
}
// 更好的写法
arr.forEach(function (item) {
console.log(item);
});
避免阻塞UI
JavaScript的运行是阻塞UI的,当我们在进行一些耗时的操作时,应尽量使用setTimeout或Promise等异步方法,以避免阻塞UI。
setTimeout(function () {
// 执行耗时的操作...
}, 0);
使用合适的数据结构和算法
使用合适的数据结构和算法是优化程序性能的基础。例如,当我们需要查找数据时,可以使用对象或Map,而不是数组;当我们需要频繁地添加或移除数据时,可以使用链表,而不是数组。
// 使用对象进行查找
var obj = { 'John': 1, 'Emma': 2, 'Tom': 3 };
console.log(obj['John']);
// 使用Map进行查找
var map = new Map();
map.set('John', 1);
map.set('Emma', 2);
map.set('Tom', 3);
console.log(map.get('John'));
避免不必要的闭包
虽然闭包在某些情况下很有用,但是它们也会增加额外的内存消耗,因此我们应该避免不必要的闭包。
// 不必要的闭包
function createFunction() {
var name = 'John';
return function () {
return name;
}
}
// 更好的方式
function createFunction() {
var name = 'John';
return name;
}
避免使用with
语句
with
语句会改变代码的作用域,这可能会导致性能问题,因此我们应该避免使用它。
// 不好的写法
with (document.getElementById('myDiv').style) {
color = 'red';
backgroundColor = 'black';
}
// 好的写法
var style = document.getElementById('myDiv').style;
style.color = 'red';
style.backgroundColor = 'black';
避免在for-in
循环中使用hasOwnProperty
hasOwnProperty
方法会查询对象的整个原型链,这可能会影响性能。在for-in
循环中,我们应该直接访问对象的属性。
// 不好的写法
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key + ': ' + obj[key]);
}
}
// 好的写法
for (var key in obj) {
console.log(key + ': ' + obj[key]);
}
使用位操作进行整数运算
在进行整数运算时,我们可以使用位操作符,它比传统的算术运算符更快。
// 不好的写法
var half = n / 2;
// 好的写法
var half = n >> 1;
避免在循环中创建函数
在循环中创建函数会导致性能问题,因为每次迭代都会创建一个新的函数实例。我们应该在循环外部创建函数。
// 不好的写法
for (var i = 0; i < 10; i++) {
arr[i] = function () {
return i;
}
}
// 好的写法
function createFunction(i) {
return function () {
return i;
}
}
for (var i = 0; i < 10; i++) {
arr[i] = createFunction(i);
}
使用Web Worker进行多线程处理
JavaScript默认是单线程运行的,但我们可以使用Web Worker来进行多线程处理,以提升程序的运行效率。
// 主线程
var worker = new Worker('worker.js');
worker.onmessage = function (event) {
console.log('Received message ' + event.data);
}
worker.postMessage('Hello Worker');
// worker.js
self.onmessage = function(event) {
console.log('Received message ' + event.data);
self.postMessage('You said: ' + event.data);
};
使用WebAssembly进行性能关键部分的开发
WebAssembly是一种新的编程语言,它的代码运行速度接近原生代码,非常适合于进行性能关键部分的开发。例如,我们可以用WebAssembly来开发图形渲染、物理模拟等复杂任务。
// 加载WebAssembly模块
WebAssembly.instantiateStreaming(fetch('module.wasm'))
.then(result => {
// 调用WebAssembly函数
result.instance.exports.myFunction();
});
使用内存池来管理对象
当我们频繁地创建和销毁对象时,可以使用内存池来管理这些对象,以避免频繁地进行内存分配和垃圾回收,从而提升性能。
class MemoryPool {
constructor(createObject, resetObject) {
this.createObject = createObject;
this.resetObject = resetObject;
this.pool = [];
}
acquire() {
return this.pool.length > 0 ? this.resetObject(this.pool.pop()) : this.createObject();
}
release(obj) {
this.pool.push(obj);
}
}
var pool = new MemoryPool(
() => { return {}; },
obj => { for (var key in obj) { delete obj[key]; } return obj; }
);
使用双缓冲技术进行绘图
当我们需要进行频繁的绘图操作时,可以使用双缓冲技术,即先在离屏画布上进行绘图,然后一次性将离屏画布的内容复制到屏幕上,这样可以避免屏幕闪烁,并且提升绘图性能。
var offscreenCanvas = document.createElement('canvas');
var offscreenContext = offscreenCanvas.getContext('2d');
// 在离屏画布上进行绘图...
offscreenContext.fillRect(0, 0, 100, 100);
// 将离屏画布的内容复制到屏幕上
context.drawImage(offscreenCanvas, 0, 0);
使用WebGL进行3D渲染
WebGL是一种用于进行3D渲染的Web标准,它提供了底层的图形API,并且能够利用GPU进行加速,非常适合于进行复杂的3D渲染。
var canvas = document.getElementById('myCanvas');
var gl = canvas.getContext('webgl');
// 设置清空颜色缓冲区的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 清空颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
使用Service Workers进行资源缓存
Service Workers可以让你控制网页的缓存策略,进一步减少HTTP请求,提升网页的加载速度。例如,你可以将一些不常变化的资源文件预先缓存起来。
// 注册一个service worker
navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(error) {
console.log('ServiceWorker registration failed: ', error);
});
// service-worker.js
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll([
'/style.css',
'/script.js',
// 更多资源...
]);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
使用内容分发网络(CDN)
你可以将静态资源(如JavaScript、CSS、图片等)上传到CDN,这样用户可以从离他们最近的服务器下载资源,从而提高下载速度。
<!-- 从CDN加载jQuery库 -->
<script src="https://cdn.example.com/jquery.min.js"></script>
使用HTTP/2进行资源加载
HTTP/2支持头部压缩和多路复用,可以更高效地加载资源。如果你的服务器和用户的浏览器都支持HTTP/2,那么你可以使用它来提高性能。
// 假设我们有一个HTTP/2库
var client = new Http2Client('https://example.com');
client.get('/resource1');
client.get('/resource2');
使用Web Socket进行数据通信
如果你需要频繁地与服务器进行数据交换,可以使用Web Socket,它比HTTP有更低的开销。
var socket = new WebSocket('ws://example.com/socket');
socket.addEventListener('open', function() {
socket.send('Hello, server');
});
socket.addEventListener('message', function(event) {
console.log('Received message from server: ' + event.data);
});
使用Progressive Web Apps(PWA)技术
PWA可以让你的网站在离线时仍然可用,并且可以被添加到用户的主屏幕,提供类似于原生应用的体验。PWA需要使用Service Workers和Manifest等技术。
// 注册Service Worker
navigator.serviceWorker.register('/service-worker.js');
// 检测是否支持Manifest
if ('manifest' in document.createElement('link')) {
var link = document.createElement('link');
link.rel = 'manifest';
link.href = '/manifest.json';
document.head.appendChild(link);
}
使用WebRTC进行实时通信
WebRTC是一种提供实时通信(RTC)能力的技术,允许数据直接在浏览器之间传输,对于需要实时交互的应用,如视频聊天、实时游戏等,可以使用WebRTC来提高性能。
var pc = new RTCPeerConnection();
// 发送offer
pc.createOffer().then(function(offer) {
return pc.setLocalDescription(offer);
}).then(function() {
// 发送offer给其他浏览器...
});
// 收到answer
pc.setRemoteDescription(answer);
使用IndexedDB存储大量数据
如果你需要在客户端存储大量数据,可以使用IndexedDB。与localStorage相比,IndexedDB可以存储更大量的数据,并且支持事务和索引。
var db;
var request = indexedDB.open('myDatabase', 1);
request.onupgradeneeded = function(event) {
db = event.target.result;
var store = db.createObjectStore('myStore', { keyPath: 'id' });
store.createIndex('nameIndex', 'name');
};
request.onsuccess = function(event) {
db = event.target.result;
};
request.onerror = function(event) {
// 错误处理...
};
使用Web Push进行后台消息推送
Web Push允许服务器在后台向浏览器推送消息,即使网页已经关闭。这需要在Service Worker中使用Push API和Notification API。
// 请求推送通知的权限
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
console.log('Push notification permission granted');
}
});
// 订阅推送服务
navigator.serviceWorker.ready.then(function(registration) {
registration.pushManager.subscribe({ userVisibleOnly: true }).then(function(subscription) {
console.log('Push subscription: ', subscription);
});
});
// 在Service Worker中接收和显示推送通知
self.addEventListener('push', function(event) {
var data = event.data.json();
self.registration.showNotification(data.title, data);
});
通过服务器端渲染(SSR)改善首次页面加载性能
服务器端渲染意味着在服务器上生成HTML,然后将其发送到客户端。这可以加快首次页面加载速度,因为用户可以直接看到渲染好的页面,而不必等待JavaScript下载并执行。这对于性能要求很高的应用来说,是一种有效的优化手段。
// 服务器端
app.get('/', function(req, res) {
const html = ReactDOMServer.renderToString(<MyApp />);
res.send(`<!DOCTYPE html><html><body>${html}</body></html>`);
});
利用HTTP3/QUIC协议进行资源传输
HTTP3/QUIC协议是HTTP/2的后续版本,采用了全新的底层传输协议(即QUIC),以解决HTTP/2中存在的队头阻塞(Head-of-line Blocking)问题,从而进一步提高传输性能。如果你的服务器和用户的浏览器都支持HTTP3/QUIC,那么可以考虑使用它进行资源传输。
使用Service Worker与Background Sync实现离线体验
通过Service Worker,我们可以将网络请求与页面渲染解耦,从而实现离线体验。并且,结合Background Sync,我们可以在用户离线时提交表单或同步数据,并在用户重新联网时自动重试。
// 注册Service Worker
navigator.serviceWorker.register('/sw.js');
// 提交表单
fetch('/api/submit', {
method: 'POST',
body: new FormData(form)
}).catch(() => {
// 如果请求失败,使用Background Sync重试
navigator.serviceWorker.ready.then(reg => {
return reg.sync.register('sync-submit');
});
});
// 在Service Worker中监听sync事件
self.addEventListener('sync', event => {
if (event.tag === 'sync-submit') {
event.waitUntil(submitForm());
}
});
使用PostMessage进行跨文档通信
如果你的应用涉及到多个窗口或者iframe,你可能需要在他们之间进行通信。使用postMessage
方法可以进行跨文档通信,而不用担心同源策略的问题。
// 父窗口向子iframe发送消息
iframeElement.contentWindow.postMessage('Hello, child', 'https://child.example.com');
// 子iframe接收消息
window.addEventListener('message', function(event) {
if (event.origin !== 'https://parent.example.com') return;
console.log('Received message: ' + event.data);
});
使用Intersection Observer进行懒加载
Intersection Observer API可以让你知道一个元素何时进入或离开视口,这对于实现图片或者其他资源的懒加载来说非常有用。
var images = document.querySelectorAll('img.lazy');
var observer = new IntersectionObserver(function(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
var img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
images.forEach(img => {
observer.observe(img);
});
利用OffscreenCanvas进行后台渲染
OffscreenCanvas
API使得开发者可以在Web Worker线程中进行Canvas渲染,这可以提高渲染性能,尤其是在进行大量或者复杂的Canvas操作时。
var offscreen = new OffscreenCanvas(256, 256);
var ctx = offscreen.getContext('2d');
// 在后台线程中进行渲染...
利用Broadcast Channel进行跨标签页通信
Broadcast Channel
API提供了一种在同源的不同浏览器上下文之间进行通信的方法,这对于需要在多个标签页之间同步数据的应用来说非常有用。
var channel = new BroadcastChannel('my_channel');
// 发送消息
channel.postMessage('Hello, other tabs');
// 接收消息
channel.onmessage = function(event) {
console.log('Received message: ' + event.data);
};
使用Web Cryptography API进行安全操作
Web Cryptography API
提供了一组底层的加密API,使得开发者可以在Web环境中进行安全的密码学操作,例如哈希、签名、加密、解密等。
window.crypto.subtle.digest('SHA-256', new TextEncoder().encode('Hello, world')).then(function(hash) {
console.log(new Uint8Array(hash));
});
使用Blob对象进行大型数据操作
Blob
对象代表了一段二进制数据,可以用来处理大量的数据,比如文件。它们可以直接从服务端获取,或者由客户端生成,这对于处理大型数据或者二进制数据很有用。
var fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', function(event) {
var file = event.target.files[0];
var reader = new FileReader();
reader.onload = function(event) {
var contents = event.target.result;
processContents(contents);
};
reader.readAsArrayBuffer(file);
});
使用Page Visibility API进行页面可见性调整
Page Visibility API
提供了一种方式来判断页面是否对用户可见。利用这个API,你可以在页面不可见时停止或减慢某些操作,例如动画或视频,从而节省CPU和电池使用。
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
pauseAnimation();
} else {
resumeAnimation();
}
});
使用WeakMap和WeakSet进行高效的内存管理
在处理大量数据时,如果不小心可能会产生内存泄漏。WeakMap和WeakSet可以用来保存对对象的引用,而不会阻止这些对象被垃圾回收。这在一些特定的应用场景中,例如缓存、记录对象状态等,可能非常有用。
let cache = new WeakMap();
function process(obj) {
if (!cache.has(obj)) {
let result = /* 对obj进行一些复杂的处理... */
cache.set(obj, result);
}
return cache.get(obj);
}
使用requestAnimationFrame进行动画处理
requestAnimationFrame能够让浏览器在下一次重绘之前调用指定的函数进行更新动画,这样可以保证动画的流畅性,并且减少CPU的使用。
function animate() {
// 更新动画...
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
使用CSS3动画替代JavaScript动画
CSS3动画不仅可以提供更好的性能,还可以在主线程之外运行,从而避免阻塞UI。因此,我们应该尽可能地使用CSS3动画替代JavaScript动画。
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.myDiv {
animation: fadeIn 2s ease
-in-out;
}
避免回流和重绘
回流和重绘是浏览器渲染过程中的两个步骤,它们对性能影响很大。优化的关键在于尽可能减少触发回流和重绘的操作,例如一次性修改样式,避免布局抖动等。
var el = document.getElementById('my-el');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 尽量避免上面的写法,以下为优化后的写法
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
使用CSS3硬件加速提高渲染性能
使用 CSS3 的 transform 属性做动画效果,可以触发硬件加速,从而提高渲染性能。
element.style.transform = 'translate3d(0, 0, 0)';
避免使用同步布局
同步布局(或强制布局)是指浏览器强制在 DOM 修改和计算样式之后,立即进行布局。这会中断浏览器的优化过程,导致性能下降。一般出现在连续的样式修改和读取操作之间。
let div = document.querySelector('div');
// 写样式
div.style.width = '100px';
// 读样式,导致同步布局
let width = div.offsetWidth;
// 再写样式
div.style.height = width + 'px'; // 强制布局
为避免这个问题,可以将读操作移到所有写操作之后:
let div = document.querySelector('div');
// 写样式
div.style.width = '100px';
// 写样式
div.style.height = '100px';
// 读样式
let width = div.offsetWidth;
使用ArrayBuffer处理二进制数据
ArrayBuffer 提供了一种处理二进制数据的高效方式,例如图像,声音等。
var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}
利用ImageBitmap提高图像处理性能
ImageBitmap
对象提供了一种在图像处理中避免内存拷贝的方法,可以提高图像处理的性能。
var img = new Image();
img.src = 'image.jpg';
img.onload = function() {
createImageBitmap(img).then(function(imageBitmap) {
// 在这里使用 imageBitmap
});
};
作者:linwu
来源:juejin.cn/post/7249991926307864613