拒绝Redux文档“毒害” 一个项目告诉你Redux最新真正哲学
之前分享过几篇关于React技术栈的原创文章:
今天进一步剖析一个实际案例:Uber APP 移动网页版。 如果你对React技术栈没有多大兴趣,或者不是很了解,也没有关系。因为读下来,你会发现,这篇文章的真谛其实在于性能优化上。 本文灵感和主体内容翻译自Narendra N Shetty的文章:How I built a super fast Uber clone for mobile web,同时进行了大量扩充以及深挖。 出发点和产品雏形很早以来,相信大家都会认同一个观点:移动端流量超越PC端是不争的事实。对于前端开发者来说,移动端web的开发同样非常有趣,也充满挑战。 这不,Uber最近发布了最新版本APP,全新样式,体验超棒。于是,笔者决定使用React来从零开始构建一个新的属于自己的Uber。 开发期间,笔者花费了很多时间在基础组件和样式搭建上。这环节中,主要应用了Uber官方开放的React地图库,并在地图上“目的地”和“起始点”之间采用svg-overlay和html-overlay去绘制路线。 最终的基本交互可以参考下面Gif图:
走上优化之路现在,我们有基本的产品形态了。目前面临的问题在于提高产品的各方面性能体验。我使用了Chrome Lighthouse去检验产品的性能表现。最终得到的结果为:
wow... 接下来,什么也不说了,撸起袖子,想办法去优化吧。 优化方法1-代码分离(Code Splitting)我最开始想到并使用的方法就是:Code Splitting(代码分离),正好我们可以借助webpack来实现这项技术。
因为笔者使用了React技术栈,并采用了react-router,所以代码的划分(split)就可以按照路由和加载时机进行。具体操作可以使用react-router的getComponent api来实现: <Route path="home" name="home" getComponent={(nextState,cb) => { require.ensure([],(require) => { cb(null,require('../components/Home').default); },'HomeView'); }}> 只有当对应路由被请求时,相应的组件才会被加载呈现。 同时,笔者使用了webpack的CommonChunkPlugin插件提取第三方代码。这是出于什么考虑呢? 细心的读者可能会发现上面的code splitting也许会存在一个问题: { 'entry': { 'app': './src/index.js','vendor': [ 'react','react-redux','redux','react-router','redux-thunk' ] },'output': { 'path': path.resolve(__dirname,'./dist'),'publicPath': '/','filename': 'static/js/[name].[hash].js','chunkFilename': 'static/js/[name].[hash].js' },'plugins': [ new webpack.optimize.CommonsChunkPlugin({ name: ['vendor'],// 公共块的块名称 minChunks: Infinity,// 最小被引用次数,最小是2。传递Infinity只是创建公共块,但不移动模块。 filename: 'static/js/[name].[hash].js',// 公共块的文件名 }),] } 这样子,我们把公共代码(react、react-redux、redux、react-router、redux-thunk)专门抽取到vendor模块中。 通过上述方法,笔者欣喜地发现:
这无疑是激动人心的。 优化方法2-Server side rendering(服务端直出)也许你一直在听说过“服务端渲染”或者“服务端直出”这样的名词。但是从未实践过,也从来没有了解过他的意义。好吧,这里我先描述一下,到底什么是服务端直出。 服务端直出,其实简单总结为服务器在接到来自浏览器第一次请求时,便返回一个“初步最终”HTML文档。这个HTML文档已经进行了数据拼接。这样用户能以最快的时间看到首屏的效果,当然这个效果是“阉割版”的,非最终版本。 这种方式主要是针对“前后分离”的传统模式。传统模式中,服务器返回HTML文档,之后浏览器解析文档标签,拉取CSS,之后拉取JS文件。JS文件加载完成之后,执行JS内容,并发送请求获取数据。最终,将数据渲染在页面上。 由此,Server side rendering方式将JS请求数据的过程放在了服务器上,甚至对于数据与HTML结合处理也可以在服务器上做。 这样一来,主要就是加快了首屏渲染时间。当然,使用服务端渲染,还能够优化前端渲染难以克服的SEO问题。 理论理解起来很简单,难处就在于服务器端环境的前端脚本如何处理,如何与客户端保持一致。 在这个项目中,我使用了Express作为nodeJS框架,结合react-router完成: server.use((req,res)=> { match({ 'routes': routes,'location': req.url },(error,redirectLocation,renderProps) => { if (error) { res.status(500).send(error.message); } else if (redirectLocation) { res.redirect(302,redirectLocation.pathname + redirectLocation.search); } else if (renderProps) { // Create a new Redux store instance const store = configureStore(); // Render the component to a string const html = renderToString(<Provider store={store}><RouterContext {...renderProps} /></Provider>); const preloadedState = store.getState(); fs.readFile('./dist/index.html','utf8',function (err,file) { if (err) { return console.log(err); } let document = file.replace(/<div id="app"></div>/,`<div id="app">${html}</div>`); document = document.replace(/'preloadedState'/,`'${JSON.stringify(preloadedState)}'`); res.setHeader('Cache-Control','public,max-age=31536000'); res.setHeader("Expires",new Date(Date.now() + 2592000000).toUTCString()); res.send(document); }); } else { res.status(404).send('Not found') } }); }); 通过上述方法,我们欣喜地发现:
这无疑是令人振奋的。 优化方法3-Compressed static assets(压缩静态文件)压缩文件,当然是一个容易想到而且行之有效的措施。为此,我使用了webpack的CompressionPlugin插件: { 'plugins': [ new CompressionPlugin({ test: /.js$|.css$|.html$/ }) ] } 同时,使用express-static-gzip来对服务端进行配置: server.use('/static',expressStaticGzip('./dist/static',{ 'maxAge': 31536000,setHeaders: function(res,path,stat) { res.setHeader("Expires",new Date(Date.now() + 2592000000).toUTCString()); return res; } })); express-static-gzip是一个处于express.static之上的中间件。如果对于指定路径的文件没有找到压缩版本,就使用为压缩版本进行返回。 经过此处理,我们缩短了400ms时间,OK,现在First meaningful paint时间为546.6ms.
优化方法4-Caching(缓存)截止到此,我们已经从最初的19189.9ms已经优化到546ms,我们当然继续可以在客户端进行静态文件缓存来使得加载时间变得更短。 笔者使用了sw-toolbox搭配service workers进行。
Service Worker Toolbox provides some simple helpers for use in creating your own service workers. Specifically,it provides common caching strategies for dynamic content,such as API calls,third-party resources,and large or infrequently used local resources that you don't want precached. 简单翻译下: 也许到这里你一头雾水,没关系,我们从最初开始,了解一下什么是service worker:
service worker是一段脚本,与web worker一样,也是在后台运行。 而sw-toolbox,顾名思义,就是service worker一个toolbox,具体我们看代码: toolbox.router.get('(.*).js',toolbox.fastest,{ 'origin':/.herokuapp.com|localhost|maps.googleapis.com/,'mode':'cors','cache': { 'name': `js-assets-${VERSION}`,'maxEntries': 50,'maxAgeSeconds': 2592e3 } }); 上面代码的意思是,我们对于get类型的请求,当请求内容为js脚本时,应用toolbox.fastest handler处理。 考虑周到的读者可能会想,上面是对于支持Service worker的浏览器,那么对于不支持的浏览器呢?我们干脆设置: res.setHeader("Expires",new Date(Date.now() + 2592000000).toUTCString()); 通过这样处理,我们来直观感受一下页面加载瀑布流:
优化方法5-Preload and then load(预加载/延后加载)如果你还没听说过“Preload”,不要紧。我们这就来了解一下:
换成你能听明白的话来说: 这样子,究竟有什么意义呢? preload的出现就是为了优化这个过程。 对于不支持preload的浏览器,笔者使用了prefetch来处理。 这些新标准其实很有意思,里面的内容远不止这些。有兴趣的同学可以自行了解,也欢迎与我讨论。 回到正题,我在head标签中使用: <link rel="preload" ... as="script"> 最终优化的结果如图:
总结其实,使用React+Webpack做出一个Uber已经不是重点了。真正激动人心的是整套流程的优化之路。我们使用了大量成熟的、未成熟(新技术),希望对读者有所启发! Happy Coding! PS: 作者Github仓库,欢迎通过代码各种形式交流。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |