一、浏览器渲染过程
- 浏览器重绘(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)单独处理的属性。
二、网页交互
- 防抖(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优点
- Hooks组件复用逻辑相比高阶组件复用逻辑更易维护
比如componentDidMount/shouldComponentUpdate中,可能就会有大量逻辑代码,包括网络请求,一些事件的监听,hooks的好处: 面向生命周期编程变成了面向业务逻辑编程,便于维护 - hook可以将state细微化,便于组件的拆分, class时期,setState后需要对比整个虚拟dom的状态,对一个复杂页面,几十个状态需要对比耗费性能。而hook阶段只需要对比一个值即可,性能更佳。
用hook代替高阶组件
- 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 .....