性能优化一直是前端避不开的话题,本文将会从如何加快首屏渲染和如何优化运行时性能两方面入手,谈一谈我的在项目中的性能优化手段(不说 CSS 放头部,减小 HTTP 请求等方式)javascript
一说到懒加载,可能更多人想到的是图片懒加载,但懒加载能够作的更多html
咱们在项目中常常会用到第三方的 JS 文件,好比 网易云盾、明文加密库、第三方的客服系统(zendesk)等,在接入这些第三方库时,他们的接入文档经常会告诉你,放在 head 中间,可是其实这些可能就是影响你首屏性能的凶手之一,咱们只须要用到它时,在把他引入便可前端
编写一个简单的加载脚本代码:java
/** * 动态加载脚本 * @param url 脚本地址 */
export function loadScript(url: string, attrs?: object) {
return new Promise((resolve, reject) => {
const matched = Array.prototype.find.call(document.scripts, (script: HTMLScriptElement) => {
return script.src === url
})
if (matched) {
// 若是已经加载过,直接返回
return resolve()
}
const script = document.createElement('script')
if (attrs) {
Object.keys(attrs).forEach(name => {
script.setAttribute(name, attrs[name])
})
}
script.type = 'text/javascript'
script.src = url
script.onload = resolve
script.onerror = reject
document.body.appendChild(script)
})
}
复制代码
有了加载脚本的代码后,咱们配合加密密码登陆使用react
// 明文加密的方法
async function encrypt(value: string): Promise<string> {
// 引入加密的第三方库
await loadScript('/lib/encrypt.js')
// 配合 JSEncrypt 加密
const encrypt = new JSEncrypt()
encrypt.setPublicKey(PUBLIC_KEY)
const encrypted = encrypt.encrypt(value)
return encrypted
}
// 登陆操做
async function login() {
// 密码加密
const password = await encrypt('12345')
await fetch('https://api/login', {
method: 'POST',
body: JSON.stringify({
password,
})
})
}
复制代码
这样子就能够避免在用到以前引入 JSEncrypt,其他的第三方库相似webpack
在如今的前端开发中,咱们可能比较少会运用 script 标签引入第三方库,更多的仍是选择 npm install
的方式来安装第三方库,这个 loadScript 就无论用了git
咱们用 import()
的方式改写一下上面的 encrypt
代码github
async function encrypt(value: string): Promise<string> {
// 改成 import() 的方式引入加密的第三方库
const module = await import('jsencript')
// expor default 导出的模块
const JSEncrypt = module.default
// 配合 JSEncrypt 加密
const encrypt = new JSEncrypt()
encrypt.setPublicKey(PUBLIC_KEY)
const encrypted = encrypt.encrypt(value)
return encrypted
}
复制代码
import()
相对于 loadScript 来讲,更方便的一点是,你一样能够用来懒加载你项目中的代码,或者是 JSON 文件等,由于经过 import()
方式懒加载的代码或者 JSON 文件,一样会通过 webpack
处理web
例如项目中用到了城市列表,可是后端并无提供这个 API,而后网上找了一个 JSON 文件,却并不能经过 loadScript
懒加载把他引入,这个时候就能够选择 import()
npm
const module = await import('./city.json')
console.log(module.default)
复制代码
这些懒加载的优化手段有不少可使用场景,好比渲染 markdown 时用到的 markdown-it
和 highlight.js
,这两个包加起来是很是大的,彻底能够在须要渲染的时候使用懒加载的方式引入
有了脚本懒加载,那么同理可得.....CSS 懒加载
/** * 动态加载样式 * @param url 样式地址 */
export function loadStyleSheet(url: string) {
return new Promise((resolve, reject) => {
const matched = Array.prototype.find.call(document.styleSheets, (styleSheet: HTMLLinkElement) => {
return styleSheet.href === url
})
if (matched) {
return resolve()
}
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = url
link.onload = resolve
link.onerror = reject
document.head.appendChild(link)
})
}
复制代码
路由懒加载也算是老生常谈的一个优化手段了,这里很少介绍,简单写一下
function lazyload(loader: () => Promise<{ default: React.ComponentType<any> }>) {
const LazyComponent = React.lazy(loader)
const Lazyload: React.FC = (props: any) => {
return (
<React.Suspense fallback={<Spinner/>}>
<LazyComponent {...props}/>
</React.Suspense>
)
}
return Lazyload
}
const Login = lazyload(() => import('src/pages/Home'))
复制代码
CDN 可讲的也很少,大概就是根据请求的 IP 分配一个最近的缓存服务器 IP,让客户端去就近获取资源,从而实现加速
提及首屏优化,不得不提的一个就是服务端优化。如今的 SPA 应用是利用 JS 脚原本渲染。在脚本执行完以前,用户看到的会是空白页面,体验很是很差。
服务端渲染的原理:
react-dom/server
中的 renderToString
方法将 jsx
代码转为 HTML 字符串,而后将 HTML 字符串返回给浏览器ReactDOM.hydrate
进行 "激活",就是将整个逻辑在浏览器再跑一遍,为应用添加点击事件等交互服务端渲染的大概过程就是上面说的,可是第一步说的,服务端只是将 HTML 字符串返回给了浏览器。咱们并无为它注入 JS 代码,那么第三步就完成不了了,没法在浏览器端运行。
因此在第一步以前须要一些准备工做,好比将应用代码打包成两份,一份跑在 Node 服务端,一份跑在浏览器端。具体的过程这里就不描述了,有兴趣的能够看我写的另外一篇文章: TypeScript + Webpack + Koa 搭建自定义的 React 服务端渲染
顺便安利一下写的一个服务端渲染库:server-renderer
这个并不算是懒加载,只能说算不阻碍主要的任务运行,对加快首屏渲染多多少少有点意思,略过。
有对 webpack 打包生成后的文件进行分析过的小伙伴们确定都清楚,咱们的代码可能只占所有大小的 1/10 不到,更多的仍是第三方库致使了整个体积变大
咱们安装第三方库的时候,只是执行npm install xxx
便可,可是他的整个文件大小咱们是不清楚的,这里安利一下网站: bundlephobia.com
能够看到,只要输入 npm 包名称,就能够看到使用的 npm 包通过压缩和 Gzip 后的文件大小。这里能够看到,咱们通过使用的 moment 大小既然达到了 65.9 KB!!可是咱们可能只会用到 moment().format(template)
等为数很少的方法
因此这是一个性价比很是低的库
可是你把 bundlephobia 拉倒底部的时候,会发现,他会给你推荐一些相似的包
好比 dayjs
既然只要 2.76KB,而且经过他的简介能够看出,它提供了和 moment 一个的 API,也就是说,大部分状况下,你能够使用 dayjs 代替 moment
若是项目中大量引入了 moment,不容易替换的话,也可使用 webpack
配置解决
const webpackConfig = {
resolve: {
alias: {
moment: 'dayjs',
}
}
}
复制代码
而后咱们只是换了一个 npm 包,就将大小减小了 60 KB 左右
这部分可能不少人有不一样的意见,不认同的小伙伴能够跳过
先说明我对
antd
没意见,我也很喜欢这个强大的组件库
antd
对于不少 React 开发的小伙伴来讲,多是一个必不可少的配置,由于他方便、强大
可是咱们先看一下他的大小
587.9 KB!这对于前端来讲是一个很是大的数字,官方推荐使用 babel-plugin-import
来进行按需引入,可是你会发现,有时候你只是引入了一个 Button
,整个打包的体积增长了200 KB
这是由于它并无对 Icon 进行按需加载,它不能肯定你项目中用到了那些 Icon,因此默认将全部的 Icon 打包进你的项目中,对于没有用到的文件来讲,让用户加载这部分资源是一个极大的浪费
像 antd
这类 组件库是一个很是全面强大的组件库,像 Select
组件,它提供了很是全面的用法,可是咱们并不会用到全部功能,没有用到的对于咱们来讲一样是一种浪费
可是不否定像 antd
这类组件库确实能提升咱们的的开发效率
优化 React 的运行时性能,说到底就是减小渲染次数或者是减小 Diff 次数
由一个常见的聊天功能提及,设计以下
在开始编写以前对它分析一下,不能一股脑的将全部东西放在一个组件里面完成
import * as React from 'react'
import { useFormInput } from 'src/hooks'
const InputBar: React.FC = () => {
const input = useFormInput('')
return (
<div className='input-bar'> <textarea placeholder='请输入消息,回车发送' value={input.value} onChange={input.handleChange} /> </div> ) } export default InputBar 复制代码
import * as React from 'react'
const ConversationHeader: React.FC = () => {
return (
<div className='conversation-header'> <img src='' alt='' /> <h4>聊天对象</h4> </div> ) } export default ConversationHeader 复制代码
import * as React from 'react'
import ConversationHeader from './Header'
import MessageList from './MessageList'
import InputBar from './InputBar'
const Conversation: React.FC = () => {
const [messages, setMessages] = React.useState([])
const send = () => {
// 发送消息
}
React.useEffect(
() => {
socket.onmessage = () => {
// 处理消息
}
},
[]
)
return (
<div className='conversation'>
<ConversationHeader/>
<MessageList messages={messages}/>
<InputBar send={send}/>
</div>
)
}
export default Conversation
复制代码
这样子不知不觉中,三个组件的分工其实也比较明确了
可是咱们会发现,外层的父组件中的 messages 更新,一样会引发三个子组件的更新
那么如何进一步优化呢,就须要结合 React.memo
了
React.memo 和 PureComponent 有点相似,React.memo 会对 props 的变化作一个浅比较,来避免因为 props 更新引起的没必要要的性能消耗
咱们就能够结合 React.memo
修改一下
// 其余的同理
export default React.memo(ConversationHeader)
复制代码
而后咱们接着看一下 React.memo
的定义
function memo<T extends ComponentType<any>>( Component: T, propsAreEqual?: (prevProps: Readonly<ComponentProps<T>>, nextProps: Readonly<ComponentProps<T>>) => boolean ): MemoExoticComponent<T>; 复制代码
能够看到,它支持咱们传入第二个参数 propsAreEqual
,能够由这个方法让咱们手动对比先后 props 来决定更新与否
export default React.memo(MessageList, (prevProps, nextProps) => {
// 简单的对比演示,当新旧消息长度不同时,咱们更新 MessageList
return prevProps.messages.length === nextProps.messages.length
})
复制代码
另外,由于 React.memo
会对先后 props 作浅比较,那此对于咱们很清楚业务中有绝对能够不更新的组件,尽管他会接受不少 props,咱们想连浅比较的消耗的避过的话,就能够传入一个返回值为 true 的函数
const propsAreEqual = () => true
React.memo(Component, propsAreEqual)
复制代码
若是会被大量使用的话,咱们就抽成一个函数
export function withImmutable<T extends React.ComponentType<any>>(Component: T) {
return React.memo(Component, () => true)
}
复制代码
分离静态不更新组件,减小性能消耗,这部分其实跟 Vue 3.0 的 静态树提高 相似
虽然利用 React.memo
能够避免重复渲染,可是它是针对 props 变化避免的
可是因为自身 state
或者 context
引发的没必要要更新,就能够运用 useMemo
和 useCallback
进行分析优化
由于 Hooks 出来后,咱们大多使用函数组件(Function Component)
的方式编写组件
const FunctionComponent: React.FC = () => {
// 层架复杂的对象
const data = {
// ...
}
const callback = () => {}
return (
<Child data={data} callback={callback} /> ) } 复制代码
所以在函数组件的内部,每次更新都会从新走一遍函数内部的逻辑,在上面的例子中,就是一次次建立 data
和 callback
那么在使用 data
的子组件中,因为 data 层级复杂,虽然里面的值可能没有变化,可是因为浅比较的缘故,依然会致使子组件一次次的更新,形成性能浪费
一样的,在组件中每次渲染都建立一个复杂的组件,也是一个浪费,这时候咱们就可使用 useMemo
进行优化
const FunctionComponent: React.FC = () => {
// 层架复杂的对象
const data = React.memo(
() => {
return {
// ...
}
},
[inputs]
)
const callback = () => {}
return (
<Child data={data} callback={callback} /> ) } 复制代码
这样子的话,就能够根据 inputs
来决定是否从新计算 data
,避免性能消耗
在上面用 React.memo
优化的例子,也可使用 useMemo
进行改造
const ConversationHeader: React.FC = () => {
return React.useMemo(() => {
return (
<div className='conversation-header'> <img src='' /> <h4>专业法币交易</h4> </div> ) }, []) } export default ConversationHeader 复制代码
像上面说的,useMemo
相对于 React.memo
更好的是,能够规避 state
和 context
引起的更新
可是 useMemo
和 useCallback
一样有性能损耗,并且每次渲染都会在 useMemo
和 useCallback
内部重复的建立新的函数,这个时候如何取舍?
useMemo
useCallback 同理....
本文为边想边写,可能有地方不对,能够指出
还有一些优化,或者跟业务相连比较精密的优化,可能给忽略了,下次想起来了再整理分享出来
感谢阅读!