前端性能优化方法

一、浏览器渲染过程

  1. 浏览器重绘(Repaint)和回流(Reflow)

重绘(Repaint):当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘(Repaint)。

回流(Reflow):当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。

回流必将引起重绘,重绘不一定会引起回流。

会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的 DOM 元素
  • 激活 CSS 伪类(例如:hover)
  • 查询某些属性或调用某些方法
  • 一些常用且会导致回流的属性和方法
    clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、
    getBoundingClientRect()、scrollTo()
    性能影响
    回流比重绘的代价要更高。
    有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器会对频繁的回流或重绘操作进行优化:浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。

使用 transform 和 opacity 属性更改来实现动画
在 CSS 中,transforms 和 opacity 这两个属性更改不会触发重排与重绘,它们是可以由合成器(composite)单独处理的属性。

二、网页交互

  1. 防抖(debounce)/节流(throttle)
    防抖是控制次数,节流时控制频率,如果事件一直触发,则防抖下函数在中间不会执行(最后执行一次),节流下函数按照一定的频率执行。

debounce:在第一次触发事件时,不立即执行函数,而是给出一个期限值比如200ms,然后:
如果在200ms内没有再次触发事件,那么就执行函数
如果在200ms内再次触发事件,那么当前的计时取消,重新开始计时

效果:如果短时间内大量触发同一事件,只会执行一次函数。
实现:

function debounce(fn,delay){
  let timer  = null;
  return function () {
    if(timer){
      clearTimeout(timer);
    }
    timer = setTimeout(fn,delay);
  }
}

定义:对于短时间内连续触发的事件(如滚动事件),在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间


节流:让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活

效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。

实现:

funtion throttler(fn, delay) {
  let valid = true;
  return function () {
    if(!valid) {
      return false;
     }
    valid = false;
    setTimeout( () => {
      fn();
      valid = true;
    },delay)
   }
}
/* 请注意,节流函数并不止上面这种实现方案,
   例如可以完全不借助setTimeout,可以把状态位换成时间戳,然后利用时间戳差值是否大于指定间隔时间来做判定。
   也可以直接将setTimeout的返回的标记当做判断条件-判断当前定时器是否存在,如果存在表示还在冷却,并且在执行fn之后消除定时器表示激活,原理都一样
    */

定义:对于短时间内连续触发的事件,函数执行一次后会暂时失效,过了n秒后才能再次生效。

三、React性能优化

三(一)React渲染优化

props的改变(被动渲染),和state改变(主动渲染)会导致react进行重新渲染(进行dom diff)

1. 绑定事件尽量不要使用箭头函数,即不要在jsx内写内联函数
<Child handerClick={() => { this.handleClick() }}/>

原因有:每一次渲染更新这段jsx都会产生新的函数对象(即使直接绑定的DOM元素);
Child 会因为这个新生成的箭头函数而进行更新,每一次的props都是新的,在未给Child任何特殊更新限定条件的时候,从而产生Child 组件的不必要渲染

解决问题:类组件(有状态组件)绑定事件直接指向类中的函数;函数组件(无状态组件)函数定义使用useMemo/useCallback
有状态组件

class index extends React.Component{
    handerClick=()=>{
        console.log(666)
    }
    handerClick1=()=>{
        console.log(777)
    }
    render(){
        return <div>
            <ChildComponent handerClick={ this.handerClick }  />
            <div onClick={ this.handerClick1 }  >hello,world</div>
        </div>
    }
}

无状态组件

function index(){
    const handerClick1 = useMemo(()=>()=>{
       console.log(777)
    },[])  /* [] 存在当前 handerClick1 的依赖项*/
    const handerClick = useCallback(()=>{ console.log(666) },[])  /* [] 存在当前 handerClick 的依赖项*/
    return <div>
        <ChildComponent handerClick={ handerClick }  />
        <div onClick={ handerClick1 }  >hello,world</div>
    </div>
}
2. 不要使用inline定义的方法或Object为props传值 (传递给子组件的固定对象提前定义好一个变量)
// 每一次渲染都会产生一个对象给style
//  都会被认为是一个style这个prop发生了变化,因为是一个新的对象
<Foo style={{ color:"red" }}

// 对象提前定义
const fooStyle = { color: "red" }; // 可以放在构造函数中或者函数组件外面
<Foo style={fooStyle} />
3. 循环/列表正确使用key

key目的就是在一次循环中,找到与新节点对应的老节点,复用节点,节省开销
增加key后,React就不是diff,而是直接使用insertBefore操作移动组件位置,而这个操作是移动DOM节点最高效的办法
在选取Key值时尽量不要用索引号,因为如果当数据的添加方式不是顺序添加,而是以其他方式(逆序,随机等),会导致每一次添加数据,每一个数据值的索引号都不一样,这就导致了Key的变化,而当Key变化时,React就会认为这与之前的数据值不相同,会多次执行渲染,会造成大量的性能浪费

4. 组件尽可能的细分,比如一个input+list组件,可以将list分成一个PureComponent,只在list数据变化时更新。否则在input值变化页面重新渲染的时候,list也需要进行不必要的DOM diff

当一个组件的props或者state改变时,React通过比较新返回的元素和之前渲染的元素来决定是否有必要更新实际的DOM。当他们不相等时,React会更新DOM。

在一些情况下,你的组件可以通过重写这个生命周期函数shouldComponentUpdate来提升速度, 它是在重新渲染过程开始前触发的。 这个函数默认返回true,可使React执行更新。

5. 减少不必要的props引起的重绘

在 class Component 中可以使用shouldComponentUpdate或者PureComponent减少重复渲染。
Hooks组件则可以使用React.memo
普通的 React.memo和PureComponent很像,对props做一层浅比较,如果没发生变更则不执行重绘

const Child = ()=><span>test</span>
export default React.memo(Child);

React.memo支持第二个参数compare, 返回 true 时,不会触发更新,来手动判断是否需要渲染。例如对于一个普通受控组件,当defaultValue发生变更时无需重绘组件,所以可以用下面的代码实现。

function MyComponent({ defaultValue, value, onChange }) {
  return null;
}

function compare(prevProps, props) {
  return prevProps.value === props.value && 
         prevProps.onChange === props.onChange;
}

export default React.memo(MyComponent, compare);
6. 减少不必要state引起的重绘

useState的一条最佳实践是将state尽可能的颗粒化,但是在异步回调中同时更新多个状态时会触发多次渲染(在 react 的 event handler 内部同步的多次 setState 会被 batch 为一次更新,但是在一个异步的事件循环里面多次 setState,react 不会 batch。可以使用 ReactDOM.unstable_batchedUpdates 来强制 batch)

import { unstable_batchUpdateds } from 'react-dom';
function Home() {
     const [userInfo, setUserInfo] = useState(null);
     const [loading, setLoading] = useState(true);
   
     useEffect(() => {
         asyncFetchUser().then(() => {
// ------------------
         unstable_batchUpdateds(()=>{
             setUserInfo({});
             setLoading(false);
         })
// -----------------
         });
     }, []);

     console.log('render');
     return null;
 }
7. 善用useMemo/useCallback/React.memo

对于无状态组件,数据更新就等于函数上下文的重复执行。那么函数里面的变量,方法就会重新声明,可以用useMemo/useCallback将常量或者函数缓存下来避免重复执行

8. 合理使用状态管理(dva/redux-sage/...)

状态管理的主要作用:一 就是解决跨层级组件通信问题 。二 就是对一些全局公共状态的缓存。
自身组件单独的数据可直接再组件内部请求数据,避免数据走了一遍状态管理,最终还是回到了组件本身的情况

三(二)React获取数据优化

使用reselect库:
在使用Redux进行数据的传递时,特别是经常有重复性的数据传递操作时,可以使用reselect库在内部对数据进行缓存处理,在重复调用时便可使用缓存快速加载,加强性能

React hooks 对比class优点

  1. Hooks组件复用逻辑相比高阶组件复用逻辑更易维护
    比如componentDidMount/shouldComponentUpdate中,可能就会有大量逻辑代码,包括网络请求,一些事件的监听,hooks的好处: 面向生命周期编程变成了面向业务逻辑编程,便于维护
  2. hook可以将state细微化,便于组件的拆分, class时期,setState后需要对比整个虚拟dom的状态,对一个复杂页面,几十个状态需要对比耗费性能。而hook阶段只需要对比一个值即可,性能更佳。

用hook代替高阶组件

  1. useSelector/useDispatch代替高阶函数connect
import { useDispatch, useSelector } from 'react-redux';
//....
 const dispatch = useDispatch();
useEffect (()=>{
  dispatch({
    type:'', // ...
  })
},[])
// ....
 const { progressList, loading } = useSelector(
    ({
      dataManage,
      loading,
    }: {
      dataManage: any;
      loading: any;
    }) => ({
      ...dataManage,
      loading: loading.effects['dataManage/fetchProgressList'],
    }),
  );
return .....
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容