11 综合应用
大约 8 分钟
性能优化有哪些?
性能优化一句话总结:是让「看到页面」这件事情尽量早、尽量轻、尽量流程。
可以分为六个方面:
- 网络加载优化;
- 渲染层优化;
- JavaScript 执行优化;
- 图片与资源优化;
- 构建层优化(webpack/vite)
- 用户体验提升优化(非性能指标,但影响观感)
一、网络加载优化(资源到达浏览器前)
技术点 | 说明 |
---|---|
CDN | 加速静态资源分发 |
HTTP/2 / HTTP/3 | 多路复用、头部压缩 |
Gzip / Brotli 压缩 | 减小资源体积,默认推荐开启 |
缓存策略 | 强缓存(Cache-Control, Expires) 协商缓存(ETag, Last-Modified) |
懒加载(LazyLoad) | 图片、资源按需加载 |
Preload / Prefetch / Preconnect | 提前加载关键资源、DNS 预解析 |
代码拆分 / 按需加载 | 不打大包,按路由/组件拆 |
Tree Shaking | 移除未用的代码(ESM 前提) |
异步加载第三方脚本 | async / defer 避免阻塞渲染 |
资源合并 | 图标合并(icon font / sprite),请求数更少 |
二、渲染层优化(资源到了后,尽快变成页面)
技术点 | 说明 |
---|---|
SSR / SSG | 首屏提前生成,减少白屏时间 |
Skeleton 骨架屏 | 内容加载前展示结构框架 |
Critical CSS | 首屏 CSS 内联,避免 FOUC |
预加载字体 / 资源 | font-display: swap ,避免字体闪 |
Virtual List 虚拟滚动 | 长列表优化渲染数量 |
组件懒加载 | React.lazy / Vue defineAsyncComponent |
DOM diff 优化 | 减少重渲染(React memo ,Vue v-once ) |
keep-alive(Vue) | 缓存切页组件,状态保留 |
requestIdleCallback | 主线程空闲时执行任务 |
GPU 合成优化 | transform , will-change 触发硬件加速 |
合并 DOM 操作 | 减少 Layout / Reflow 次数 |
三、JavaScript 执行优化(JS 别拖累页面)
技术点 | 说明 |
---|---|
减少大对象、大循环 | 会阻塞主线程、影响渲染帧率 |
拆组件 / 拆任务 | 大组件分片加载,大任务分帧执行 |
防抖 / 节流 | 事件优化,减少重复触发 |
React: memo, useMemo, useCallback | 控制子组件渲染更新 |
Web Worker | 把计算移出主线程 |
异步 import() | 动态导入非关键模块 |
错误监控 / 捕获 | 避免错误阻断渲染流程 |
四、图片与媒体优化
技术点 | 说明 |
---|---|
WebP / AVIF 格式 | 更小更清晰,浏览器支持广泛 |
srcset + sizes | 按设备分辨率加载合适图 |
懒加载 | loading="lazy" / IntersectionObserver |
雪碧图 / iconfont / SVG | 减少图标请求数 |
图片压缩 | tinypng , imagemin , Webpack 插件压图 |
视频优化 | poster , preload , autoplay 控制加载方式 |
五、构建层优化(Webpack / Vite)
技术点 | 说明 |
---|---|
按需加载 | UI 库如 antd , element-plus 配合插件只引入用到的组件 |
babel-plugin-transform-remove-console | 删除开发时的 log |
splitChunks | 公共模块提取、缓存更稳 |
缓存配置 | Webpack/Vite 的构建缓存提升二次构建速度 |
构建结果分析工具 | 如 webpack-bundle-analyzer ,识别大包来源 |
生产环境压缩 | terser / esbuild 优化输出 |
CDN 外链引入大库 | React/Vue/jquery 改为 CDN 不打包进来 |
六、用户体验提升优化(非指标但直接影响体感)
技术点 | 说明 |
---|---|
加载进度条 / loading 效果 | 提高感知响应速度 |
交互延迟反馈 | 按钮点击立刻有反馈动画 |
骨架屏 / 占位图 | 比 loading 更稳定视觉 |
首屏静态展示、异步加载动态内容 | 保证页面第一时间“有东西”看 |
客户端缓存数据 | 保留上次状态,避免重复请求 |
路由切换动画 / 页面过渡 | 提升“丝滑感” |
常用优化工具推荐
工具 | 用途 |
---|---|
Chrome DevTools Performance | 看帧率、重排、渲染瓶颈 |
Lighthouse | 性能评分报告 |
WebPageTest / PageSpeed Insights | 网络请求、压缩建议 |
webpack-bundle-analyzer | 分析打包构成 |
SourceMap Explorer | 找出大文件来源 |
Sentry / Fundebug | 性能监控 + 错误日志 |
文字超出省略
单行文字省略
#box {
border: 1px solid #ccc;
width: 100px;
white-space: nowrap; /** 不换行 */
overflow: hidden;
text-overflow: ellipsis; /** 超出省略 */
}
多行文字省略
#box {
border: 1px solid #ccc;
width: 100px;
overflow: hidden;
display: -webkit-box; /** 将对象作为弹性伸缩盒子模型展示 */
-webkit-box-orient: vertical; /** 设置子元素排列方式 */
-webkit-line-clamp: 3; /** 显示几行, 超出省略 */
}
手写一个 getType 函数,获取详细的数据类型
常见的类型判断
typeof
: 只能判断值类型,其他就是function
和object。
instanceof
: 需要俩个参数来判断,而不是获取类型。
实现方法: Object.prototype.toString.call(obj)
进判断,返回 [object 数据类型]
。
function getType(x: any): string {
const originType = Object.prototype.toString.call(x)
const spaceIndex = originType.indexOf(' ')
const type = spaceIndex.slice(spaceIndex + 1, -1) // 空格开始, ']' 前结束
return type.toLowCase()
}
手写一个 new 对象的过程
创建对象的过程分为 3 步:
- 创建一个空对象 obj,继承 constructor 的原型;
- 将 obj 作为 this,执行 constructor,并传入参数;
- 返回 obj。
function customNem<T>(constructor: Function, ...args: any[]): T {
// 1. 创建一个空对象 obj,继承 constructor 的原型;
const obj = Object.create(constructor.prototype)
// 2. 将 obj 作为 this,执行 constructor,并传入参数;
obj.apply(obj, args)
// 3. 返回 obj。
return obj
}
instanceof 原理是什么, 请用代码表示
原理:
f instanceof Foo
表示会随着原型链 f.__proto__
向上查找,看是否能够找到 Foo.prototype
。
核心步骤:
- 排除 null 和 undefined;
- 排除值类型;
- while 循环逐级向上查找,看是否能够匹配到,直至 null。
/**
* 手写 instanceof
*/
function myInstanceof(instance: any, origin: any): boolean {
if (instance == null) return false // 排除 null undefined
const type = typeof instance
if (type !== 'object' && type !== 'function') {
// 值类型
return false
}
let tempInstance = instance // 防止修改 instance
while (tempInstance) {
// 向上查找, 最终到 null
if (tempInstance.__proto__ === origin.prototype) {
return true
} else {
tempInstance = tempInstance.__proto__
}
}
return false
}
// 功能测试
console.log(myInstanceof({}, Object))
console.log(myInstanceof([], Object))
console.log(myInstanceof('', Object))
手写 bind 函数
核心要点:
- bind 会返回一个新函数,但不会执行;
- 会绑定 this 和部分参数;
- 如果是箭头函数,无法改变 this,只改变参数。
/**
* 手写 bind 函数
* @param context bind 传入的 this
* @param bindArgs bind 传入的各个参数
*/
// @ts-ignore
Function.prototype.customBind = function (context: any, ...bindArgs: any[]) {
const self = this // 当前函数本身
return function (...args: any[]) {
const newArgs = bindArgs.concat(args) // 拼接参数
self.apply(context, newArgs)
}
}
// 功能测试
function fn(this: any, a: any, b: any, c: any) {
console.info(this, a, b, c)
}
// @ts-ignore
const fn1 = fn.customBind(10)
fn1(20, 30, 40)
// @ts-ignore
const fn2 = fn.customBind(10, 20)
fn2(30, 40)
手写 call 和 apply
区别于 bind 会返回一个新的函数(不执行),call 和 apply 会立即执行函数。
实现关键点:解决如何在函数执行时绑定 this。
解决方案:利用对象的函数执行的隐式绑定。
const obj = {
x: 100,
fn() {
console.log(this)
},
}
obj.fn() // 此时 this 指向 obj 本身,隐式绑定。谁调用指向谁。
构建顺序:
- 排除 null ,为全局 globalThis
- 排除值类型,变为
new Object()
- 利用 Symbol 建立唯一属性,并在调用后取出该属性。
/**
* 手写 call
*/
// @ts-ignore
Function.prototype.customCall = function (context: any, ...args: any[]) {
if (context == null) context = globalThis
if (typeof context !== 'object') context = new Object(context)
const fnKey = Symbol()
context[fnKey] = this // this 为当前函数, 相当于给绑定对象添加了当前 fn 函数属性
const res = context[fnKey](...args) // 绑定了 this,相当于执行绑定对象函数属性,此时 this 为绑定的对象
delete context[fnKey] // 清理掉函数属性, 防止污染
return res
}
// 功能测试
function fn(this: any, a: any, b: any, c: any) {
console.info(this, a, b, c)
}
// @ts-ignore
fn.customCall({ x: 100 }, 10, 20, 30)
手写 apply 则变化较少,直接将传入的参数改为数组即可(默认为空数组):
/**
* 手写 apply
*/
// @ts-ignore
Function.prototype.customCall = function (context: any, args: any[] = []) {
if (context == null) context = globalThis
if (typeof context !== 'object') context = new Object(context)
const fnKey = Symbol()
context[fnKey] = this // this 为当前函数, 相当于给绑定对象添加了当前 fn 函数属性
const res = context[fnKey](...args) // 绑定了 this,相当于执行绑定对象函数属性,此时 this 为绑定的对象
delete context[fnKey] // 清理掉函数属性, 防止污染
return res
}
// 功能测试
function fn(this: any, a: any, b: any, c: any) {
console.info(this, a, b, c)
}
// @ts-ignore
fn.customCall({ x: 100 }, [10, 20, 30])
遍历数组,生成 tree node
const arr = [
{ id: 1, name: 'A', parentId: 0 },
{ id: 2, name: 'A', parentId: 1 },
{ id: 3, name: 'A', parentId: 2 },
{ id: 4, name: 'A', parentId: 3 },
{ id: 5, name: 'A', parentId: 4 },
{ id: 6, name: 'A', parentId: 5 },
]
思路:
- 遍历数组
- 每个元素,生成 tree node
- 找到 parentNode,并加入它的 children。
/**
* 数组转树结构
*/
interface IArrayItem {
id: number
name: string
parentId: number
}
interface ITreeNode {
id: number
name: string
children?: ITreeNode[]
}
function convert(arr: IArrayItem[]): ITreeNode | null {
// 用于 id 和 treeNode 的映射
const idToTreeNode: Map<number, ITreeNode> = new Map()
let root = null
arr.forEach((item) => {
const { id, name, parentId } = item
// 定义 tree node 并加入 map
const treeNode: ITreeNode = { id, name }
idToTreeNode.set(id, treeNode)
// 找到 parentNode 并加入它们的 children
const parentNode = idToTreeNode.get(parentId)
if (parentNode) {
if (parentNode.children == null) parentNode.children = []
parentNode.children.push(treeNode)
}
// 找到根节点
if (parentId === 0) root = treeNode
})
return root
}
const arr = [
{ id: 1, name: 'A', parentId: 0 },
{ id: 2, name: 'A', parentId: 1 },
{ id: 3, name: 'A', parentId: 2 },
{ id: 4, name: 'A', parentId: 3 },
{ id: 5, name: 'A', parentId: 4 },
{ id: 6, name: 'A', parentId: 5 },
]
const tree = convert(arr)
console.info(tree)
Loading...