性能优化面试题
网络层面
DNS预解析
DNS-prefetch
是一种 DNS 预解析技术。它会在请求跨域资源之前,预先解析并进行DNS缓存,以减少真正请求时DNS解析导致的请求延迟。对于打开包含有许多第三方连接的网站,效果明显。
添加ref属性为“dns-prefetch”的link标签。一般放在在html的head中。
<link rel="dns-prefetch" href="//xxx.download.com">
href
的值就是要预解析的域名,对应后面要加载的资源或用户有可能打开链接的域名。
应用浏览器缓存
浏览器缓存是浏览器存放在本地磁盘或者内存中的请求结果的备份。当有相同请求进来时,直接响应本地备份,而无需每次都从原始服务器获取。这样不仅提升了客户端的响应效率,同时还能缓解服务器的访问压力。
其间,约定何时、如何使用缓存的规则,被称为缓存策略。分为强缓存和协商缓存。
整个缓存执行的过程大致如下:
①. 请求发起,浏览器判断本地缓存,如果有且未到期,则命中强缓存。浏览器响应本地备份,状态码为200。控制台Network中size那一项显示disk cache;
②. 如果没有缓存或者缓存已过期,则请求原始服务器询问文件是否有变化。服务器根据请求头中的相关字段,判断目标文件新鲜度;
③. 如果目标文件没变更,则命中协商缓存,服务器设置新的过期时间,浏览器响应本地备份,状态码为304;
④. 如果目标文件有变化,则服务器响应新文件,状态码为200。浏览器更新本地备份。
以Nginx举例。强缓存的配置字段是expires
,它接受一个数字,单位是秒。
server {
listen 8080;
location / {
root /Users/zhp/demo/cache-koa/static;
index index.html;
# 注意try_files会导致缓存配置不生效
# try_files $uri $uri/ /index.html;
expires 60;
}
}
在响应头加上强缓存所需的Exprise
和Cache-Control
字段
app.use(async (ctx) => {
// 1.根据访问路径读取指定文件
const content = fs.readFileSync(`./static${ctx.path}`, "utf-8");
// 2.设置缓存
ctx.response.set("Cache-Control", "max-age=60");
ctx.response.set('Exprise', new Date(new Date().getTime()+60*1000));
// 3.设置响应
ctx.body = content;
});
静态资源CDN
概念
CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
核心功效总结起来就两点:
①. 通过负载均衡技术 ,为用户的请求选择最佳的服务节点;
②. 通过内容缓存服务,提高用户访问响应速度。
使用高版本的HTTP协议
HTTP/1.1的持久连接和管道化技术、2.0的多路复用和首部压缩
代码层面
优化DOM操作
概念
众所周知,浏览器的渲染成本是极其昂贵的。通过合并DOM操作,可以避免频繁的触发重排重绘,以提升渲染效率。
优化DOM操作的最佳实践,莫过于大名鼎鼎的虚拟DOM。
virtual DOM 虚拟DOM,用普通JS对象来描述DOM结构,因为不是真实DOM,所以称之为虚拟DOM
它的价值在于:
①. 查找 JS 对象的属性要比查询 DOM 树的开销要小;
②. 当数据驱动频繁触发DOM操作的时候,所有变化先反映在这个 JS 对象上。最终在一个宏任务(EventLoop机制)中统一执行所有变更,达成合并DOM操作的效果;
③. 可以方便的通过比较新旧两个虚拟DOM(Diff算法),最大程度的缩小DOM变更范围
事件委托
概念
简单来讲,就是当我们绑定事件时,不直接绑到目标元素,而是绑到其父/祖先元素上的绑事件策略。
这样做有两个好处:①. 页面监听的事件少;②. 当新增子节点时,不需要再绑定事件。
实操
以”鼠标放到li上对应的li背景变灰“这个需求场景举例
- 正常绑事件:
<ul>
<li>item1</li>
<li>item2</li>
<li>item3</li>
<li>item4</li>
<li>item5</li>
<li>item6</li>
</ul>
<script>
$("li").on("mouseover", function () {
$(this)
.css("background-color", "#ddd")
.siblings()
.css("background-color", "white");
});
</script>
- 利用事件委托:
$("ul").on("mouseover", function (e) {
$(e.target)
.css("background-color", "#ddd")
.siblings()
.css("background-color", "white");
});
防抖和节流
防抖与节流都是为了优化单位时间内大量事件触发,存在的性能问题。它们只是效果不同,适用场景不同。
- 防抖。单位时间多次连续触发,最终只执行最后的那一次。核心原理是延迟执行,期间但凡有新的触发就重置定时器。
经典应用场景:搜索框中的实时搜索,等待用户不再输入内容后再做接口查询
节流。单位时间内事件仅触发一次。核心原理是加锁,只有满足一定间隔时间才执行。
function throttle(fn) {
// 1、通过闭包保存一个标记
let canRun = true;
return function(...args) {
// 2、在函数开头判断标志是否为 true,不为 true 则中断函数
if(!canRun) {
return;
}
// 3、将 canRun 设置为 false,防止执行之前再被执行
canRun = false;
// 4、定时器
setTimeout( () => {
fn.call(this, args); //如果需要立即执行,把改行移到定时器外层
// 5、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
canRun = true;
}, 1000);
};
}经典应用场景:滚动事件等高频触发的场景;按钮防重复点击等
图片懒加载
图片懒加载是针对图片加载时机的一种优化,在一些图片量比较大的网站(比如电商网站首页,或者团购网站、小游戏首页等),如果我们尝试在用户打开页面的时候,就把所有的图片资源加载完毕,那么很可能会造成白屏、卡顿等现象。
懒加载的意思就是让浏览器只加载可视区内的图片,可视区外的大量图片不进行加载,当页面滚动到后面去的时候再进行加载。避免资源浪费的同时,可以使页面加载更流畅。
图片只是载体,懒加载贯彻的是按需加载的思路。举一反三,分页查询、路由懒加载、模块异步加载,都是该类别的常用优化
构建层面
路由懒加载
概念:
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
实操
下面是VueRouter关于路由懒加载的官方示例
// 将
// import UserDetails from './views/UserDetails'
// 替换成
const UserDetails = () => import('./views/UserDetails')
const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})
核心实现就两点:
①. 使用了ES6 的动态导入方法import(),异步的加载模块;
②. 打包工具,在构建时自动识别并打包成单独的代码块。
我们还可以通过行内注释/* webpackChunkName: "about" */
(Webpack语法),指定代码块的名称,和把多个路由源码构建到同一个块中。
// router.js
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
Externals排除依赖
概念
Webpack的externals
配置项允许我们从输出的 bundle 中排除指定依赖,排除的依赖不参与构建。
通常用于配合较大体积第三方依赖使用CDN的场景。
实操
以在vue-cli项目中 CDN vue举例
首先在public/index.html添加script引用
// public/index.html
<!DOCTYPE html>
<html lang="">
<head>
...
<script src="https://lib.baomitu.com/vue/2.6.11/vue.min.js"></script>
</head>
<body>
...
</body>
</html>
复制代码使用webpack配置项externals排除vue的依赖
// vue.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
configureWebpack:{
plugins: [
new BundleAnalyzerPlugin() // 用于输出下图中的打包分析报告 npm run build --report
],
externals: {
vue: 'Vue',
},
}
}
TreeShaking按需引入
概念
TreeShaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。
概念早就有了,实现的话则是在ES6之后。主要得益于ES6 Module模块的编译时加载,使得静态分析成为可能。
Webpack 4 正式版本,扩展了该项能力。在vue-cli创建的项目中我们不需要任何额外配置,就有效果。
但,当improt第三方插件时,实际并没有生效。比如lodash
import debounce from 'lodash/debounce'; // 3.35kb
import { debounce } from 'lodash'; // 72.48kb
因为,它的生效需要满足一些条件:
进阶优化
服务端渲染
SSR是Server Side Render(服务端渲染)的简称,与之相对应的是Client Side Render(客户端渲染)。
- 服务端渲染:在服务端完成页面插值/数据组装,直接返回包含有数据的页面。
- 客户端渲染:客户端分别请求页面静态资源和接口数据,然后操作DOM赋值到页面。
其实,Web世界诞生的初始,只有服务端渲染这一种方式。 那时.net、jsp如日中天,那时还只有一种程序员,不分前后端。直到Ajax技术的出现,允许人们不刷新页面的获取数据,客户端渲染的大门就此打开,一发而不可收拾。前后端分离、单页应用的流行,更是一步步的把客户端渲染的疆域推向极致。
现如今,SSR一般只存在于对首屏时间有苛刻要求、以静态内容为主和需要SEO的场景。
webWorkers
web worker 是运行在后台的 JavaScript,不会影响页面的性能。
原理就是开子线程
Worker
接口会生成真正的操作系统级别的线程,线程可以执行任务而不阻塞 UI 线程。
一般用于处理像密集型运算等耗费 CPU 资源的任务。
实操
无米之炊。我这阅历并没有遇到需要Worker的场景,仅说下自己联想到的唯二信息:①.有些插件比如psfjs有这块的应用,因为它的构建结果中有xxx.worker.js;②. Node有线程相关的API(child_process),在构建的场景有较多应用。