跳到主要内容

性能优化

编码心得

  1. 遵循数据不可变原则,特别是不要去操作外部传入的变量(当你见到一个变量透传进若干个函数,每个函数都可能操作这个变量时,调试起来就是地狱难度)
  2. 遵循函数式编程的原则,尽量减少不需要的过程变量,这可以有利于保证代码逻辑的清晰和简洁。当变量只在一个地方使用时,可以考虑去除这个变量。
  3. 遵循组合大于继承的原则,适当考虑增加函数抽象层,哪怕是非常简单的逻辑判断,如果在100个地方都需要这个逻辑,那么将整个逻辑抽象进一个函数内,后续当这个逻辑变更时可以更好的修改;另外对于类型的使用,应该尽量基于现有类型进行组合,而不是创建一个结构类似的全新类型。
  4. 保持项目目录结构的清晰,根据功能的不同来进行模块的划分,每个模块内部都管理着该模块中才需要使用的代码(组件、样式、接口定义、常量、枚举、方法),只要当某些内容同时被多个模块所依赖时才将其提升到上层模块中,从而实现高内聚低耦合的效果。
  5. 不宜过早优化。在保证日常编码准确的前提下,不宜在微小的优化点上使用更复杂的实现,必要时可以对整个系统的性能进行整体评估(如借助开发者工具进行性能分析),对性能瓶颈进行专项优化更加有效。
  6. 不要重复造轮子。常见逻辑可以使用社区知名库实现(如lodash,ahooks等),自己造轮子容易导致维护困难。

React性能

  1. 当纯组件的Props可能为字面量对象时,建议使用深层Equal函数

    const App = memo(App2, isEqual)

    <App list={[1, 2, 3]} />
  2. useSelector返回字面量对象时,建议使用shadowEqual或者isEqual

    function App() {
    useSelector(() => {
    // code
    return [1, 2, 3]
    }, shallowEqual)
    }
  3. 当全局状态改变时,useSelector的回调函数会被频繁调用,注意不要放入计算密集的操作,必要时可以使用reselect缓存

  4. 使用useContext时,需要注意一旦Providervalue发生了改变,那么使用useContext的组件就会触发重新渲染。解决办法:

    1. 拆分Context
    2. 拆分组件
    3. useMemo

其他

  • 减少HTTP请求
    • 合并资源文件(CSS, JS, 雪碧图)
    • 压缩资源文件
    • 图片懒加载(借助IntersectionObserver)
    • 合理设置HTTP缓存, CDN缓存
  • 首屏渲染优化
    • 代码分割,路由懒加载
    • 骨架屏
  • 代码优化
    • 不用table (流式布局)
    • 不用with, eval
  • CSS优化
    • CSS3(transform, opacity)硬件加速
    • 频繁操作DOM时,可以先用display: none使其脱离文档流再进行DOM操作
    • 对于复杂的动画效果,可以使用position: absolute使其脱离文档流
  • JS优化
    • 函数防抖,函数节流

图片懒加载

常规方法(使用offsetTop - scrollTop 或者 getBoundingClientRect())

<body>
<div class="blank">
// 很长的元素,使图片开始不在视口里
</div>
<div class="image" data-url="C:/Users/Messiah/Pictures/image.png">
// 想要懒加载的图片
</div>
<script type="text/javascript">
let image = document.querySelector('.image')

window.onscroll = throttle(() => {
// 方法一,使用offsetTop - scrollTop
if (image.offsetTop - document.documentElement.scrollTop < document.documentElement.clientHeight)
{
let url = image.dataset.url
image.style.backgroundImage = `url(${url})`
}

// 方法二,使用getBoundingClientRect
if (image.getBoundingClientRect().top < document.documentElement.clientHeight) {
let url = image.dataset.url
image.style.backgroundImage = `url(${url})`
}
}, 200)

// 节流函数
function throttle (fn, time) {
let canRun = true
return function () {
if (!canRun) return false
canRun = false
setTimeout(() => {
fn()
canRun = true
}, time)
}
}

</script>
</body>

https://zhuanlan.zhihu.com/p/55311726

offsetParent定义: 一个元素的已定位(position不为static)的父元素, 类似于绝对定位中已经定位的父元素,如果一个元素没有已经定位的父元素,则该元素的offsetParent为body。

此例子中image.offsetTop,image没有已经定位的父元素,则image的offsetParent为body。

如果我们的代码结构如下,

    <body>
<div class="outer">
<div class="blank">

</div>
<div class="image">

</div>
</div>

</body>

滚动条在outer上,我们应该给outer加一个样式

.outer {
position: relative;
}

那么,image.offsetParent就是outer,我们就可以继续使用以下代码来判断图片是否进入视口

image.offsetTop - outer.scrollTop < outer.clientHeight

IntersectionObserver

// 使用IntersectionObserver,十分方便
let io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return
else {
let el = entry.target
el.style.backgroundImage = `url(${el.dataset.url})`
io.unobserve(el)
}

})
})
io.observe(document.querySelector(".image"))
// <div class="image" data-url="https://xxx.com/1.jpg" ></div>

这个API同样能用来实现无限滚动(Infinity Scroll)

setTimeout/setInterval和requestAnimationFrame

setTimeout/setInterval

setTimeout(fn, n)会在指定的时间n毫秒后,将指定的回调函数fn放进任务队列中,因此并不是n秒后就会执行回调函数。

setTimeout(fn, 0) 即使传参为0ms,最短其实为4 ms。

缺点: 一般显示器刷新频率为60HZ,即16.6ms刷新一次屏幕。setTimeout可能会掉帧。

requestAnimationFrame

function myAnimation() {
// do something
requestAnimationFrame(myAnimation)
}

requestAnimationFrame(myAnimation)

它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次