一、jquery
选择器性能优化建议
- 总是从#id选择器来继承
这是jQuery选择器的一条黄金法则。jQuery选择一个元素最快的方法就是用ID来选择了。
$( ‘#content’ ).hide();
或者从ID选择器继承来选择多个元素:
$( ‘#content p’ ).hide();
- 在class前面使用tag
jQuery中第二快的选择器就是tag选择器(如$(‘head’)),因为它和直接来自于原生的Javascript方法getElementByTagName()。所以最好总是用tag来修饰class(并且不要忘了就近的ID)
var receiveNewsletter = $( ‘#nslForm input.on’ );
jQuery中class选择器是最慢的,因为在IE浏览器下它会遍历所有的DOM节点。尽量避免使用class选择器。也不要用tag来修饰ID。下面的例子会遍历所有的div元素来查找id为’content’的那个节点:
var content = $( ‘div#content’ ); // 非常慢,不要使用
用ID来修饰ID也是画蛇添足:
var traffic_light = $( ‘#content #traffic_light’ ); // 非常慢,不要使用
- 使用子查询
将父对象缓存起来以备将来的使用
var header = $( ‘#header’ );
var menu = header.find( ‘.menu’ );
// 或者
var menu = $( ‘.menu’ , header);
- 利用强大的链式操作
采用jQuery的链式操作比缓存选择器更有效:
$( ‘li.menu-item’ ).click( function () {alert( ‘test click’ );})
.css( ‘display’ , ‘block’ )
.css( ‘color’ , ‘red’ )
fadeTo( 2 , 0.7 );
优化DOM操作建议
- 缓存jQuery对象
将你经常用的元素缓存起来:
var header = $( ‘#header’ );
var divs = header.find( ‘div’ );
var forms = header.find( ‘form’ );
- 当要进行DOM插入时,将所有元素封装成一个元素
直接的DOM操作很慢。尽可能少的去更改HTML结构。
var menu = ‘
’ ;$( ‘#header’ ).prepend(menu);
// 千万不要这样做:
$( ‘#header’ ).prepend( ‘ ’ );
for ( var i = 1 ; i < 100 ; i++) {
$( ‘#menu’ ).append( ‘
- ’ + i + ‘
- ’ );
}- 采用jQuery的内部函数data()来存储状态
不要忘了采用.data()函数来存储信息:
$( ‘#head’ ).data( ‘name’ , ‘value’ );
// 之后在你的应用中调用:
$( ‘#head’ ).data( ‘name’ );- 使用jQuery utility函数
jQuery的utility函数
关于优化事件性能的建议
- 推迟到$(window).load
有时候采用 ( w i n d o w ) . l o a d ( ) 比 (window).load()比 (window).load()比(document).ready()更快,因为后者不等所有的DOM元素都下载完之前执行。你应该在使用它之前测试它。
- 使用Event Delegation
当你在一个容器中有许多节点,你想对所有的节点都绑定一个事件,delegation很适合这样的应用场景。使用Delegation,我们仅需要在父级绑定事件,然后查看哪个子节点(目标节点)触发了事件。当你有一个很多数据的table的时候,你想对td节点设置事件,这就变得很方便。先获得table,然后为所有的td节点设置delegation事件:
$( “table” ).delegate( “td” , “hover” , function (){
$( this ).toggleClass( “hover” );
});- 使用ready事件的简写
如果你想压缩js插件,节约每一个字节,你应该避免使用$(document).onready()
// 也不要使用
$(document).ready( function (){
// 代码
});
// 你可以如此简写:
$( function (){
// 代码
});- 将script放在尾部
将script放在里,浏览器解析HTML,发现script标签时,会先下载完所有这些script,再往下解析其他的HTML。讨厌的是浏览器在下载JS时,是不能多个JS并发一起下载的。不管JS是不来来自同一个host,浏览器最多只能同时下载两个JS,且浏览器下载JS时,就block掉解析其他HTML的工作[1]。将script放在头部,会让网页内容呈现滞后,导致用户感觉到卡。所以yahoo建议将script放在尾部,这样能加速网页加载。
将script放在尾部的缺点,是浏览器只能先解析完整个HTML页面,再下载JS。而对于一些高度依赖于JS的网页,就会显得慢了。所以将script放在尾部也不是最优解,最优解是一边解析页面,一边下载JS。
所以[2]提出了一种更modern的方式:使用async和defer。80%的现代浏览器都认识async和defer属性[3],这两个属性能让浏览器做到一边下载JS(还是只能同时下载两个JS),一边解析HTML。他的优点不是增加JS的并发下载数量,而是做到下载时不block解析HTML。
带async属性的script会异步执行,只要下载完就执行,这会导致script2.js可能先于script1.js执行(如果script2.js比较大,下载慢)。defer就能保证script有序执行,script1.js先执行,script2.js后执行。
二、vue项目优化
代码层面的优化
- v-if 和 v-show 区分使用场景
v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
- computed 和 watch 区分使用场景
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
- v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
(1)v-for 遍历必须为 item 添加 key
在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。
(2)v-for 遍历避免同时使用 v-if
v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。
- 长列表性能优化
Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
- 图片资源懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件:
(1)安装插件
npm install vue-lazyload --save-dev
(2)在入口文件 man.js 中引入并使用
import VueLazyload from ‘vue-lazyload’
然后再 vue 中直接使用
Vue.use(VueLazyload)
或者添加自定义选项
Vue.use(VueLazyload, {
preLoad: 1.3,
error: ‘dist/error.png’,
loading: ‘dist/loading.gif’,
attempt: 1
})(3)在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示:
- 路由懒加载
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。
路由懒加载:
const Foo = () => import('./Foo.vue') const router = new VueRouter({ routes: [ { path: '/foo', component: Foo } ] })
- 第三方插件的按需引入
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。如elementui按需引入
- 优化无限列表性能
如果你的应用存在非常长或者无限滚动的列表,那么需要采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。你可以参考以下开源项目 vue-virtual-scroll-list 和 vue-virtual-scroller 来优化这种无限列表的场景的。
- 服务端渲染 SSR or 预渲染
服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。
(1)服务端渲染的优点:
更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
更快的内容到达时间(首屏加载更快):SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;
(2)服务端渲染的缺点:
更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源,因此如果你预料在高流量环境下使用,请准备相应的服务器负载,并明智地采用缓存策略。
如果你的项目的 SEO 和 首屏渲染是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和 SEO,具体的 Vue SSR 如何实现,可以参考作者的另一篇文章《Vue SSR 踩坑之旅》。如果你的 Vue 项目只需改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin 就可以轻松地添加预渲染 。
- vue中使用keep-alive
vue2.0提供了一个keep-alive组件,用来缓存组件,避免多次加载相应的组件,减少性能消耗,但keep-alive是一把双刃剑,确实需要缓存组件的时候才使用。
Webpack 层面的优化
- Webpack 对图片进行压缩
在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片:
(1)首先,安装 image-webpack-loader :
npm install image-webpack-loader --save-dev
(2)然后,在 webpack.base.conf.js 中进行配置:
{ test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use:[ { loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { loader: 'image-webpack-loader', options: { bypassOnDebug: true, } } ] }
- 提取公共代码
如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
相同的资源被重复加载,浪费用户的流量和服务器的成本。
每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下:
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。 new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function(module, count) { return ( module.resource && /\.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, '../node_modules') ) === 0 ); } }), // 抽取出代码模块的映射关系 new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', chunks: ['vendor'] })
- 提取组件的 CSS
当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。
查阅这个构建工具各自的文档来了解更多:
webpack + vue-loader ( vue-cli 的 webpack 模板已经预先配置好)
Browserify + vueify
Rollup + rollup-plugin-vue
- 优化 SourceMap
我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap 出现了,它就是为了解决不好调式代码问题的。
SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度 )
开发环境推荐:cheap-module-eval-source-map
生产环境推荐:cheap-module-source-map
原因如下:
cheap:源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息;
module :不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个 Vue 文件报错了,我们希望能定位到具体的 Vue 文件,因此我们也需要 module 配置;
soure-map :source-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加source-map 属性;
eval-source-map:eval 打包代码的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。
基础的 Web 技术优化
- 开启 gzip 压缩
gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,gzip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右
以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下:
npm install compression --save
添加代码逻辑:
var compression = require('compression'); var app = express(); app.use(compression())
重启服务,观察网络面板里面的 response header
- CDN 的使用
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率 。
- 浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的
- 使用 Chrome Performance 查找性能瓶颈
Chrome 的 Performance 面板可以录制一段时间内的 js 执行细节及时间。使用 Chrome 开发者工具分析页面性能的步骤如下。
1.打开 Chrome 开发者工具,切换到 Performance 面板
2.点击 Record 开始录制
3.刷新页面或展开某个节点
4.点击 Stop 停止录制其它
- 切换多入口模式
main.js是整个单页面应用唯一的入口文件
在大型项目中,单一入口无法满足项目需要,原因有以下两方面:
(1)单一入口无法拆分按模块独立部署,不够灵活
(2)单一入口负载过重,业务模块较多时,点击系统导航菜单频繁切换业务模块,在IE浏览器中,浏览器内存持续上涨,达到 1.5G以上卡顿,浏览器容易崩溃。
基于以上两点原因,大型项目可以采用多入口的模式,好处有以下两点:
(1)满足页面需要分开部署的场景
(2)解决了页面卡顿和内存上涨问题
- 定时器销毁
路由离开及时销毁定时器
如果是setTimeout这种定时器,不清理就会在线程空闲后立即执行一次。如果是setInterval这种,不清理,就一直按照间隔不断的执行下去。参考博客:
Vue 项目性能优化方案三、react项目性能优化
React组件优化
- 属性传递优化
在动态页面中,免不了使用事件来监控按钮,React中便针对这种情况有相应的优化。以点击事件onClick为例,在React中,事件的声明方式有三种。
① 事件在声明时一起绑定:
② 使用函数式声明事件:③ 在按钮内声明,在constructor内绑定:
三种声明方式的比较:①②相对于③,再次执行时都要再渲染一编render()里的bind函数和函数声明式,而③的绑定函数只执行一次,并不会每次执行时都进行调用,对于性能方面,显然③方式会比①②更好,而且②相对①对性能的影响会低一点。所以三者对性能优化的优先级为③>②>①。- 多组件优化
在父组件因状态的变化更改,而子组件并没有状态变化时,若子组件随着父组件一起更新,会造成比较大的性能浪费,为减少子组件额外渲染而浪费性能,可使用:
① shouldComponentUpdate(nextProps, nextState):
根据React组件的生命周期可以知道,组件在每次更新状态时都会执行shouldComponentUpdate函数,为了减少额外渲染,可以在该函数内对当前的props/state与nextProps/nextState进行比较,如果有一致的props/state则返回fasle说明不用重新渲染该组件,以减少重新渲染造成的性能浪费。
② React.PureComponent 替换 React.Component:
在使用shouldComponentUpdate函数比较前后的props/state是否一致时,通常会涉及到深层或浅层的比较,在React默认进行的浅层比较中,可以使用React.PureComponent让组件根据传来的数据进行渲染而不是全部数据的渲染,这比自己写shouldComponentUpdate函数进行比较来的简单且性能更好,但只适用于组件只根据传进来的数据进行渲染而没有内部状态时使用,可以最大限度的提升性能。
③ ImmutableJS:
在比较props/state时,应使用深层比较的形式,但要手动写shouldComponentUpdate函数的深层比较需要写一个递归的函数,通过层层递归比较出当前值和next值的数据结构是否相同,这在性能方面是不可接受的,所以React建议也是默认的比较是只做浅对比,即不考虑props/state的数据结构,只考虑数值是否相同。所以在设计组件数据的传递时,不应做深层次的嵌套(如数据为对象,对象内有多个值,值内还是一个对象的形式)。而为了使组件在数据传递过程中保证渲染时当前值与next值一定是不相同的,facebook提供了immutable-js这个库,ImmutableJS提供了不可变的数据,即要让数据改变只能通过创建新数据的方式,而不能直接修改,这很大程度的降低了前后两个数据比较时的复杂度。
ImmutableJS小demo App组件(父组件):
Demo组件(子组件):
使用Immutable提供的is函数比较两个Immutable数据的数据结构是否相同
由于Immutable库比较大,所以如果在React中引用该库也是比较大的负担,有一个Immutablejs库的简易版叫做seamless-immutable,该库只支持Map,Set,List三种数据类型,但相对Immutable来说较小,对应用的负担也小。- Key
对于数组形式的数据,遍历时React会要求你为每一个数据值添加Key,而Key必须时独一无二的,在选取Key值时尽量不要用索引号,因为如果当数据的添加方式不是顺序添加,而是以其他方式(逆序,随机等),会导致每一次添加数据,每一个数据值的索引号都不一样,这就导致了Key的变化,而当Key变化时,React就会认为这与之前的数据值不相同,会多次执行渲染,会造成大量的性能浪费。所以只在万不得已时,才将数据的Key设为索引号。
- 使用 React.memo 进行组件记忆
React.memo 是一个高阶组件。
它很像 PureComponent,但 PureComponent 属于 Component 的类实现,而“memo”则用于创建函数组件。这里与纯组件类似,如果输入 props 相同则跳过组件渲染,从而提升组件性能。
它会记忆上次某个输入 prop 的执行输出并提升应用性能。即使在这些组件中比较也是浅层的。
你还可以为这个组件传递自定义比较逻辑。
用户可以用自定义逻辑深度对比(deep comparison)对象。如果比较函数返回 false 则重新渲染组件,否则就不会重新渲染。
function CustomisedComponen(props) { return ( <div> <b>User name: {props.name}</b> <b>User age: {props.age}</b> <b>User designation: {props.designation}</b> </div> ) } // The component below is the optimised version for the Default Componenent // The Component will not re-render if same props value for "name" property var memoComponent = React.memo(CustomisedComponent);
上面的组件将对前后两个 props 的值进行浅层比较。
如果我们将对象引用作为 props 传递给 memo 组件,则需要一些自定义登录以进行比较。在这种情况下,我们可以将比较函数作为第二个参数传递给 React.memo 函数。
假设 props 值(user)是一个对象引用,包含特定用户的 name、age 和 designation。
这种情况下需要进行深入比较。我们可以创建一个自定义函数,查找前后两个 props 值的 name、age 和 designation 的值,如果它们不相同则返回 false。
这样,即使我们将参考数据作为 memo 组件的输入,组件也不会重新渲染。
// The following function takes "user" Object as input parameter in props function CustomisedComponen(props) { return ( <div> <b>User name: {props.user.name}</b> <b>User age: {props.user.age}</b> <b>User designation: {props.user.designation}</b> </div> ) } function userComparator(previosProps, nextProps) { if(previosProps.user.name == nextProps.user.name || previosProps.user.age == nextProps.user.age || previosProps.user.designation == nextProps.user.designation) { return false } else { return true; } } var memoComponent = React.memo(CustomisedComponent, userComparator);
上面的代码提供了用于比较的自定义逻辑
- 懒加载组件
导入多个文件合并到一个文件中的过程叫打包,使应用不必导入大量外部文件。
所有主要组件和外部依赖项都合并为一个文件,通过网络传送出去以启动并运行 Web 应用。这样可以节省大量网络调用,但这个文件会变得很大,消耗大量网络带宽。
应用需要等待这个文件的加载和执行,所以传输延迟会带来严重的影响。
为了解决这个问题,我们引入代码拆分的概念。
像 webpack 这样的打包器支持就支持代码拆分,它可以为应用创建多个包,并在运行时动态加载,减少初始包的大小。
为此我们使用 Suspense 和 lazy。
import React, { lazy, Suspense } from "react"; export default class CallingLazyComponents extends React.Component { render() { var ComponentToLazyLoad = null; if(this.props.name == "Mayank") { ComponentToLazyLoad = lazy(() => import("./mayankComponent")); } else if(this.props.name == "Anshul") { ComponentToLazyLoad = lazy(() => import("./anshulComponent")); } return ( <div> <h1>This is the Base User: {this.state.name}</h1> <Suspense fallback={<div>Loading...</div>}> <ComponentToLazyLoad /> </Suspense> </div> ) } }
上面的代码中有一个条件语句,它查找 props 值,并根据指定的条件加载主组件中的两个组件。
我们可以按需懒惰加载这些拆分出来的组件,增强应用的整体性能。
假设有两个组件 WelcomeComponent 或 GuestComponents,我们根据用户是否登录而渲染其中一个。
我们可以根据具体的条件延迟组件加载,无需一开始就加载两个组件。
import React, { lazy, Suspense } from "react"; export default class UserSalutation extends React.Component { render() { if(this.props.username !== "") { const WelcomeComponent = lazy(() => import("./welcomeComponent")); return ( <div> <Suspense fallback={<div>Loading...</div>}> <WelcomeComponent /> </Suspense> </div> ) } else { const GuestComponent = lazy(() => import("./guestComponent")); return ( <div> <Suspense fallback={<div>Loading...</div>}> <GuestComponent /> </Suspense> </div> ) } } }
在上面的代码中我们没有预加载 WelcomeCompoment 和 GuestComponents 这两个组件,而是进行一个条件检查。
如果用户名存在(或相反),我们就根据指定的条件决定将某个组件作为单独的包加载。
这个方法的好处
主包体积变小,消耗的网络传输时间更少。动态单独加载的包比较小,可以迅速加载完成。
我们可以分析应用来决定懒加载哪些组件,从而减少应用的初始加载时间。
- 使用 React Fragments 避免额外标记
使用 Fragments 减少了包含的额外标记数量,这些标记只是为了满足在 React 组件中具有公共父级的要求。
用户创建新组件时,每个组件应具有单个父标签。父级不能有两个标签,所以顶部要有一个公共标签。所以我们经常在组件顶部添加额外标签,例如:在上面指定的组件中,我们需要一个额外的标签为要渲染的组件提供公共父级。
除了充当组件的父标签之外,这个额外的 div 没有其他用途。
在顶层有多个标签会导致以下错误:
要解决此问题,我们可以将元素包含在片段(fragement)中。
片段不会向组件引入任何额外标记,但它仍然为两个相邻标记提供父级,因此满足在组件顶级具有单个父级的条件。
export default class NestedRoutingComponent extends React.Component { render() { return ( <> <h1>This is the Header Component</h1> <h2>Welcome To Demo Page</h2> </> ) } }
上面的代码没有额外的标记,因此节省了渲染器渲染额外元素的工作量。
- 不要使用内联函数定义
如果我们使用内联函数,则每次调用“render”函数时都会创建一个新的函数实例。
当 React 进行虚拟 DOM diffing 时,它每次都会找到一个新的函数实例;因此在渲染阶段它会会绑定新函数并将旧实例扔给垃圾回收。因此直接绑定内联函数就需要额外做垃圾回收和绑定到 DOM 的新函数的工作。
import React from "react"; export default class InlineFunctionComponent extends React.Component { render() { return ( <div> <h1>Welcome Guest</h1> <input type="button" onClick={(e) => { this.setState({inputValue: e.target.value}) }} value="Click For Inline Function" /> </div> ) } }
上面的函数创建了内联函数。每次调用 render 函数时都会创建一个函数的新实例,render 函数会将该函数的新实例绑定到该按钮。
此外最后一个函数实例会被垃圾回收,大大增加了 React 应用的工作量。
所以不要用内联函数,而是在组件内部创建一个函数,并将事件绑定到该函数本身。这样每次调用 render 时就不会创建单独的函数实例了,参考组件如下。
import React from "react"; export default class InlineFunctionComponent extends React.Component { setNewStateData = (event) => { this.setState({ inputValue: e.target.value }) } render() { return ( <div> <h1>Welcome Guest</h1> <input type="button" onClick={this.setNewStateData} value="Click For Inline Function" /> </div> ) } }
- 避免使用内联样式属性
使用内联样式时浏览器需要花费更多时间来处理脚本和渲染,因为它必须映射传递给实际 CSS 属性的所有样式规则。
import React from "react"; export default class InlineStyledComponents extends React.Component { render() { return ( <> <b style={{"backgroundColor": "blue"}}>Welcome to Sample Page</b> </> ) } }
在上面创建的组件中,我们将内联样式附加到组件。添加的内联样式是 JavaScript 对象而不是样式标记。
样式 backgroundColor 需要转换为等效的 CSS 样式属性,然后才应用样式。这样就需要额外的脚本处理和 JS 执行工作。
更好的办法是将 CSS 文件导入组件。
- 不要在 render 方法中导出数据
Render 方法是 React 开发人员最熟悉的生命周期事件。
和其他生命周期事件不一样的是,我们的核心原则是将 render() 函数作为纯函数。纯函数对 render 方法意味着什么?
纯函数意味着我们应该确保 setState 和查询原生 DOM 元素等任何可以修改应用状态的东西不会被调用。该函数永远不该更新应用的状态。
更新组件状态的问题在于,当状态更新时会触发另一个 render 循环,后者在内部会再触发一个 render 循环,以此类推。
import React from "react"; export default class RenderFunctionOptimization extends React.Component { constructor() { this.state = { name: "Mayank" } } render() { this.setState({ name: this.state.name + "_" }); return ( <div> <b>User Name: {this.state.name}</b> </div> ); } }
在上面的代码中,每次调用 render 函数时都会更新状态。状态更新后组件将立即重新渲染。因此更新状态会导致 render 函数的递归调用。
render 函数应保持纯净,以确保组件以一致的方式运行和渲染。
- 使用 CDN
谷歌、亚马逊和微软等公司提供了许多内容分发网络。
这些 CDN 是可在你的应用中使用的外部资源。我们甚至可以创建私有 CDN 并托管我们的文件和资源。使用 CDN 有以下好处:
不同的域名。浏览器限制了单个域名的并发连接数量,具体取决于浏览器设置。假设允许的并发连接数为 10。如果要从单个域名中检索 11 个资源,那么同时完成的只有 10 个,还有 1 个需要再等一会儿。CDN 托管在不同的域名 / 服务器上。因此资源文件可以分布在不同的域名中,提升了并发能力。
文件可能已被缓存。有很多网站使用这些 CDN,因此你尝试访问的资源很可能已在浏览器中缓存好了。这时应用将访问文件的已缓存版本,从而减少脚本和文件执行的网络调用和延迟,提升应用性能。
高容量基础设施。这些 CDN 由大公司托管,因此可用的基础设施非常庞大。他们的数据中心遍布全球。向 CDN 发出请求时,它们将通过最近的数据中心提供服务,从而减少延迟。这些公司会对服务器做负载平衡,以确保请求到达最近的服务器并减少网络延迟,提升应用性能。
如果担心安全性,可以使用私有 CDN。
- 在 Web 服务器上启用 gzip 压缩
压缩是节省网络带宽和加速应用的最简单方法。
我们可以把网络资源压缩到更小的尺寸。Gzip 是一种能够快速压缩和解压缩文件的数据压缩算法。它可以压缩几乎所有类型的文件,例如图像、文本、JavaScript 文件、样式文件等。Gzip 减少了网页需要传输到客户端的数据量。
当 Web 服务器收到请求时,它会提取文件数据并查找 Accept-Encoding 标头以确定如何压缩应用。
如果服务器支持 gzip 压缩,资源会被压缩后通过网络发送。每份资源的压缩副本(添加了 Content-Encoding 标头)指定使用 gzip 解压。
然后,浏览器将内容解压缩原始版本在渲染给用户。
只是 gzip 压缩需要付出成本,因为压缩和解压缩文件属于 CPU 密集型任务。但我们还是建议对网页使用 gzip 压缩。
- 使用 Web Workers 处理 CPU 密集任务
JavaScript 是一个单线程应用,但在渲染网页时需要执行多个任务:
处理 UI 交互、处理响应数据、操纵 DOM 元素、启用动画等。所有这些任务都由单个线程处理。可以使用 worker 来分担主线程的负载。
Worker 线程在后台运行,可以在不中断主线程的情况下执行多个脚本和 JavaScript 任务。
每当需要执行长时间的 CPU 密集任务时,可以使用 worker 在单独的线程上执行这些逻辑块。
它们在隔离环境中执行,并且使用进程间线程通信与主线程交互。主线程就可以腾出手来处理渲染和 DOM 操作任务。
参考博客: