来源:京东云开发者
性能优化可分为以下几个维度:代码层面、构建层面、网络层面。
本文主要是从代码层面探索前端性能,主要分为以下 4 个小节。
使用 CSS 替代 JS 深度剖析 JS 前端算法 计算机底层
CSS 动画

let redBox = document.getElementById('redBox')let l = 10setInterval(() => {l+=3redBox.style.left = `${l}px`}, 50)
过渡(Transition) - 过渡是 CSS3 中常用的动画效果之一,通过对一个元素的某些属性进行变换,使元素在一段时间内从一个状态平滑地过渡到另一个状态。 动画(Animation) - 动画是 CSS3 中另一个常用的动画效果,其用于为一个元素添加一些复杂的动画效果,可以通过关键帧(@keyframes)来定义一串动画序列。 变换(Transform) - 变换是 CSS3 中用于实现 2D/3D 图形变换效果的一种技术,包括旋转、缩放、移动、斜切等效果。
#redBox {animation: mymove 5s infinite;}@keyframes mymove{from {left: 0;}to {left: 200px;}}
CSS 组件
在一些知名的组件库中,有些组件的大部分 props 是通过修改 CSS 样式实现的,比如 Vant 的 Space 组件。
再比如 Ant Design 的 Space 组件。
Props | 功能 | CSS样式 |
align | 对齐方式 | align-items: xxx; |
direction | 间距方向 | flex-direction: column; |
size | 间距大小 | gap: xxx; |
wrap | 是否自动换行 | flex-wrap: wrap; |
这类组件完全可以封装成 SCSS 的 mixin 实现(LESS 也一样),既能减少项目的构建体积(两个库的 Space 组件 gzip 后的大小分别为 5.4k 和 22.9k),又能提高性能。
查看组件库某个组件的体积,可访问连接:https://bundlephobia.com/。
比如下面的 space mixin:
/** 间距* size: 间距大小,默认是 8px* align: 对齐方式,默认是 center,可选 start、end、baseline、center* direction: 间距方向,默认是 horizontal,可选 horizontal、vertical* wrap: 是否自动换行,仅在 horizontal 时有效,默认是 false*/@mixin space($size: 8px, $direction: horizontal, $align: center, $wrap: false) {display: inline-flex;gap: $size;@if ($direction == 'vertical') {flex-direction: column;}@if ($align == 'center') {align-items: center;}@if ($align == 'start') {align-items: flex-start;}@if ($align == 'end') {align-items: flex-end;}@if ($align == 'baseline') {align-items: baseline;}@if ($wrap == true) {@if $direction == 'horizontal' {flex-wrap: wrap;}}}
优先考虑只使用样式实现 仅靠样式满足不了,就先增加一个标签,通过这个标签和它的两个伪元素 ::before 和 ::after 实现 一个标签实在不够,再考虑增加额外的标签
/* 三角形 */@mixin triangle($borderWidth: 10, $shapeColor: #666, $direction: up) {width: 0;height: 0;border: if(type-of($borderWidth) == 'number', #{$borderWidth} + 'px', #{$borderWidth}) solid transparent;$doubleBorderWidth: 2 * $borderWidth;$borderStyle: if(type-of($doubleBorderWidth) == 'number', #{$doubleBorderWidth} + 'px', #{$doubleBorderWidth}) solid #{$shapeColor};@if($direction == 'up') {border-bottom: $borderStyle;}@if($direction == 'down') {border-top: $borderStyle;}@if($direction == 'left') {border-right: $borderStyle;}@if($direction == 'right') {border-left: $borderStyle;}}
if-else 语句的优化
const a = 2const b = 10let cif (a > 3) { c = a + b} else { c = 2 * a}
function check(age, sex) { let msg = '' if (age > 18) { if (sex === 1) { msg = '符合条件' } else { msg = ' 不符合条件' } } else { msg = '不符合条件' }}function check(age, sex){ if (age > 18 && sex ==1) return '符合条件' return '不符合条件'}Switch 语句的优化
参考以下代码:
function getPrice(level) { if (level > 10) return 100 if (level > 9) return 80 if (level > 6) return 50 if (level > 1) return 20 return 10}function getPrice(level) { switch(level) case 10: return 100 case 9: return 80 case 8: case 7: case 6: return 50 case 5: case 4: case 3: case 2: case 1: return 20 default: return 10}
循环语句的优化
function findUserByName(users) { let user = null for (let i = 0; i < users.length; i++) { if (users[i].name === '张三') { user = users[i] } } return user}function findUserByName(users) { for (let i = 0; i < users.length; i++) { if (users[i].name === '章三') return users[i] }}function findUserByName(users) { let length = users.length for (let i = 0; i < length; i++) { if (users[i].name === '章三') return users[i] }}
let a = 10let b = 11function sum (a, b) {return a + b}
diff 算法

定义 4 个变量,分别为:oldStartIdx、oldEndIdx、newStartIdx 和 newEndIdx 判断 oldStartIdx 和 newStartIdx 是否相等 判断 oldEndIdx 和 newEndIdx 是否相等 判断 oldStartIdx 和 newEndIdx 是否相等 判断 oldEndIdx 和 newStartIdx 是否相等 同时 oldStartIdx 和 newStartIdx 向右移动;oldEndIdx 和 newEndIdx 向左移动

先进行双端比较,发现前面两个节点(A 和 B)和最后一个节点(G)是一样的,不需要移动 找到最长递增子序列 C、D、E(新旧 children 都包含的,最长的顺序没有发生变化的一组节点) 把子序列当成一个整体,内部不用进行任何操作,只需要把 F 移动到它的前面,H 插入到它的后面即可

遍历 Old 存下对应下标 Map 遍历 New,b 的下标从 1 变成了 0,不动(是左移不是右移) c 的下标从 2 变成了 1,不动(也是左移不是右移) a 的下标从 0 变成了 2,向右移动,b、c 下标都减 1 d 和 e 位置没变,不需要移动
只比较同一层级,不跨级比较 Tag 不同则删掉重建(不再去比较内部的细节) 子节点通过 key 区分(key 的重要性)
setState 真的是异步吗
clickHandler = () => {console.log('--- start ---')Promise.resolve().then(() => console.log('promise then'))this.setState({val: 1}, () => {console.log('state...', this.state.val)})console.log('--- end ---')}render() {return <div onClick={this.clickHandler}>setStatediv>}

JSX 里的事件,比如 onClick={() => {}},其实叫合成事件,区别于我们常说的自定义事件:
// 自定义事件document.getElementById('app').addEventListener('click', () => {})function fn() { // fn 是合成事件函数,内部事件同步执行 // 前置 clickHandler() // 后置,执行 setState 的 callback}友情提示:算法一般都是针对大数据量而言,区别于日常开发。
能用值类型就不用引用类型
function findPalindromeNumbers1(max) {const res = []if (max <= 0) return resfor (let i = 1; i <= max; i++) {// 转换为字符串,转换为数组,再反转,比较const s = i.toString()if (s === s.split('').reverse().join('')) {res.push(i)}}return res}
function findPalindromeNumbers2(max) {const res = []if (max <= 0) return resfor (let i = 1; i <= max; i++) {const s = i.toString()const length = s.length// 字符串头尾比较let flag = truelet startIndex = 0 // 字符串开始let endIndex = length - 1 // 字符串结束while (startIndex < endIndex) {if (s[startIndex] !== s[endIndex]) {flag = falsebreak} else {// 继续比较startIndex++endIndex--}}if (flag) res.push(res)}return res}
function findPalindromeNumbers3(max) {const res = []if (max <= 0) return resfor (let i = 1; i <= max; i++) {let n = ilet rev = 0 // 存储翻转数// 生成翻转数while (n > 0) {rev = rev * 10 + n % 10n = Math.floor(n / 10)}if (i === rev) res.push(i)}return res}
思路 1- 看似是 O(n),但数组转换、操作都需要时间,所以慢
思路 2 VS 思路3 - 操作数字更快(电脑原型就是计算器)
尽量用“低级”代码
如,输入字符串 12aBc34,输出字符串 12AbC34
function switchLetterCase(s) {let res = ''const length = s.lengthif (length === 0) return resconst reg1 = /[a-z]const reg2 = /[A-Z]for (let i = 0; i < length; i++) {const c = s[i]if (reg1.test(c)) {res += c.toUpperCase()} else if (reg2.test(c)) {res += c.toLowerCase()} else {res += c}}return res}
function switchLetterCase2(s) {let res = ''const length = s.lengthif (length === 0) return resfor (let i = 0; i < length; i++) {const c = s[i]const code = c.charCodeAt(0)if (code >= 65 && code <= 90) {res += c.toLowerCase()} else if (code >= 97 && code <= 122) {res += c.toUpperCase()} else {res += c}}return res}
从“内存”读数据
寄存器是在 CPU 内的,也是 CPU 的一部分,所以 CPU 从寄存器读写数据非常快。
二进制的位运算
function isPowerOfTwo(n) { if (n <= 0) return false let temp = n while (temp > 1) { if (temp % 2 != 0) return false temp /= 2 } return true}function isPowerOfTwo(n) { return (n > 0) && ((n & (n - 1)) == 0)}
export const enum ShapeFlags {ELEMENT = 1,FUNCTIONAL_COMPONENT = 1 << 1,STATEFUL_COMPONENT = 1 << 2,TEXT_CHILDREN = 1 << 3,ARRAY_CHILDREN = 1 << 4,SLOTS_CHILDREN = 1 << 5,TELEPORT = 1 << 6,SUSPENSE = 1 << 7,COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,COMPONENT_KEPT_ALIVE = 1 << 9,COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT}if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {...}if (hasDynamicKeys) {patchFlag |= PatchFlags.FULL_PROPSelse {if (hasClassBinding) {patchFlag |= PatchFlags.CLASS}if (hasStyleBinding) {patchFlag |= PatchFlags.STYLE}if (dynamicPropNames.length) {patchFlag |= PatchFlags.PROPS}if (hasHydrationEventBinding) {patchFlag |= PatchFlags.HYDRATE_EVENTS}}
JS 基础知识深度剖析 框架源码
CSS 动画、组件 算法 计算机底层