11 综合应用

Huy大约 8 分钟anonymousnote

性能优化有哪些?

性能优化一句话总结:是让「看到页面」这件事情尽量早、尽量轻、尽量流程。

可以分为六个方面:

  1. 网络加载优化;
  2. 渲染层优化;
  3. JavaScript 执行优化;
  4. 图片与资源优化;
  5. 构建层优化(webpack/vite)
  6. 用户体验提升优化(非性能指标,但影响观感)

一、网络加载优化(资源到达浏览器前)

技术点说明
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 函数,获取详细的数据类型

常见的类型判断

  1. typeof: 只能判断值类型,其他就是 functionobject。
  2. 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 步:

  1. 创建一个空对象 obj,继承 constructor 的原型;
  2. 将 obj 作为 this,执行 constructor,并传入参数;
  3. 返回 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 本身,隐式绑定。谁调用指向谁。

构建顺序:

  1. 排除 null ,为全局 globalThis
  2. 排除值类型,变为 new Object()
  3. 利用 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 },
]

思路:

  1. 遍历数组
  2. 每个元素,生成 tree node
  3. 找到 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...