รีดประสิทธิภาพโค้ด React ด้วย Webpack ตอนที่ 1
คุณคือนักพัฒนา React ใช่ไหม?
ไม่ว่าคุณจะลีลาเด็ดแค่ไหนมันไม่สำคัญเท่าท่ามาตรฐาน เรานิยมใช้ React คู่กับ Webpack ซึ่งเป็น Module Bundler ตัวสำคัญบนดวงดาวที่มี JavaScript เป็นศูนย์กลางจักรวาล เหตุนี้ Webpack และ React จึงเหมือนเป็นพี่น้องร่วมสาบานที่ต้องร่วมรบกันไปจนกว่าจะถึงฝั่งฝันคือ Production ของเรา
ความที่ Webpack มีบทบาทสำคัญต่อ React เมื่อเรากล่าวถึงการปรับปรุงประสิทธิภาพโค้ดที่เราเขียนด้วย React บ่อยครั้งจึงเลี่ยงไม่ได้ที่จะต้องปรับจูนคลื่นของ Webpack ให้เหมาะสมกับการใช้งานบน Production มากขึ้น
บทความนี้เราจะได้เรียนรู้ส่วนหนึ่งของการตั้งค่า Webpack เพื่อทำให้ประสิทธิภาพการใช้งาน React บน Production ของคุณดีขึ้น รออะไร? ไสเม้าส์ลงล่างโดยพลัน~
ยังไม่รู้จัก Webpack? ไม่บาปที่จะเริ่มต้นตอนนี้กับบทความ [Day #1] แนะนำ Webpack2 และการใช้งานร่วมกับ React แม้จะเก่าซักหน่อยแต่ก็ยังกรุบกริบ
เพื่อป้องกันสภาวะตบะแตก ผมแนะนำให้เพื่อนๆที่จะอ่านบทความนี้คุ้นเคยกับ Webpack อยู่พอสมควรครับ หากต้องการเป็นทางลัด คอร์ส Core React คือทางออก ณ จุดนี้ # งานขายต้องมา~
จงใช้ Scope Hoisting
Webpack นั้นเป็น Module Bundler นั่นคือทุกสรรพสิ่งไม่ว่าจะเป็น CSS JavaScript หรือรูปภาพ ล้วนถูกมองว่าเป็นโมดูล หน้าที่ของ Webpack คือการปู้ยี้ปู้ยำให้โมดูลเหล่านี้รวมก้อนและทำงานด้วยกันได้ ความเป็นจริงแต่ละโมดูลอาจไม่สามารถทำงานได้อย่างอิสระ โมดูลหนึ่งอาจต้องเรียกใช้งานโมดูลอื่นด้วย Webpack จึงต้องมีกลไกในการจัดการ Dependency ของแต่ละโมดูลด้วยเช่นกัน
1// App.js2// แต่ละไฟล์ทำการ export ฟังก์ชัน3import module1 from './module1'4import module2 from './module2'5import module3 from './module3'67console.log(module1())8console.log(module2())9console.log(module3())
จากโปรแกรมข้างต้นเราพบว่า App.js ไม่สามารถจบการทำงานได้ด้วยตนเอง สามโมดูลที่ App.js ต้องการคือ module1 module2 และ module3 ต้องถูกโหลดเข้ามาในฐานะที่เป็น Dependency ของ App.js โค้ดของเราจึงจะทำงานได้อย่างถูกต้อง หลังจากการ Bundle ของ Webpack เราจะได้ผลลัพธ์หน้าตาออกมาประมาณนี้
1;(function (modules) {2 function __webpack_require__(moduleId) {3 // โหลดโมดูล4 }56 // ทำการโหลด entry module7 // หาก App.js ของเราเป็น entry module และอยู่ในลำดับ 08 // โมดูล 0 จะถูกโหลด9 return __webpack_require__((__webpack_require__.s = 0))10})([11 /* module 0 */12 function (module, __webpack_exports__, __webpack_require__) {13 // โค้ดของ App.js14 },15 /* module 1 */16 function (module, __webpack_exports__, __webpack_require__) {17 // สมมติ module1.js เป็นโมดูล 1 โค้ดของมันก็จะอยู่ในนี้18 },19 /* module n */20 function (module, __webpack_exports__, __webpack_require__) {},21])
Webpack จะหั่นแต่ละโมดูลออกด้วยลำดับตัวเลขครับ กรณีของเรามีไฟล์ JavaScript ทั้งหมด 4 ไฟล์ (App.js module1.js module2.js และ module3.js) จึงได้ 4 โมดูล ในบรรทัดที่ 10 เพื่อนๆจะพบว่า Webpack จัดเก็บโค้ดการทำงานของแต่ละโมดูลเป็นอาร์เรย์ด้วยการห่อไว้ใน Scope ของฟังก์ชัน (Individual Function Closures) หากเราต้องการเรียกใช้งานโมดูลไหน เราสามารถเรียกผ่านฟังก์ชัน __webpack_require__
พร้อมระบุ ID ของโมดูลนั้นๆได้
แล้วไง ใครแคร์?
ก็ต้องแคร์หน่อยหละครับ เพราะวิธีการทำงานแบบนี้ของ Webpack ทำให้ขนาดของ Bundle ใหญ่เกินจำเป็น และด้วยการทำงานที่จะเข้าถึงโมดูลได้ต้องผ่าน __webpack_require__
ทุกครั้ง ทำให้โค้ดจาก Bundle นี้ช้าตามไปด้วยเช่นกัน
ย้อนกลับไปดูที่คู่แข่งอย่าง RollupJS หรือ Closure กันบ้าง ทั้งสองตัวนี้มีวิธีการทำงานที่ต่างไปจาก Webpack ทั้งสองเครื่องมือนี้ใช้วิธีการของ Scope Hoisting
เพื่อจัดการโมดูล
Scope Hoisting นั้นเป็นเทคนิคที่จะย้ายการประกาศโมดูลไปไว้ตอนต้นของฟังก์ชัน ทำให้เราสามารถเข้าถึงโมดูลเหล่านี้ในภายหลังได้อย่างง่ายได้
1(function () {2 'use strict';3 var module_0 = ...4 var module_1 = ...5 var module_n = ...6})
การมาของ Webpack 3 รอบนี้พี่เขาไม่ได้มาเล่นๆ Scope Hoisting เป็นหนึ่งในฟีเจอร์ที่ปฏิวัติขนาด Bundle ด้วยวิธีการตั้งค่าที่แสนง่าย เพียงแค่เพิ่ม ModuleConcatenationPlugin
ให้เป็นหนึ่งในปลั๊กอินของ Webpack ก็เป็นอันเรียบร้อย
1module.exports = {2 plugins: [new webpack.optimize.ModuleConcatenationPlugin()],3}
หลังจากการ Bundle ด้วยฟีเจอร์ของ ModuleConcatenationPlugin
เราจะได้ผลลัพธ์ใหม่ดังนี้
1(function(modules) {2 function __webpack_require__(moduleId) {3 // โหลดโมดูล4 }56 // ทำการโหลด entry module7 // หาก App.js ของเราเป็น entry module และอยู่ในลำดับ 08 // โมดูล 0 จะถูกโหลด9 return __webpack_require__(__webpack_require__.s = 0);10})([11 /* module 0 */12 (function(module, __webpack_exports__, __webpack_require__) {13 // โค้ดของ App.js14 var module1_defaultExport = (function() { // ... })15 var module2_defaultExport = (function() { // ... })16 var module3_defaultExport = (function() { // ... })1718 console.log(module1_defaultExport());19 console.log(module2_defaultExport());20 console.log(module3_defaultExport());21 })22]);
ขุ่นแพะ! อุทานเสียงหลงด้วยความตกใจพร้อมทาบอก~ Webpack ยังคงแยกโมดูลตามปกติ เพียงแต่โมดูลไหนที่สามารถนำมารวมใน entry module ได้ มันก็จะทำการประกาศไว้แต่ต้นก่อนใช้งานซะเลย เราเรียกโมดูลเหล่านี้ว่า Concatenated Modules
และเรียกพฤติกรรมสัตว์ป่าเช่นนี้ว่า Scope Hoisting
Scope Hoisting ช่วยให้ขนาดไฟล์ลดลง นั่นเพราะโมดูลไหนสามารถนำมาประกาศใช้ใน entry module (หรือโมดูลอื่นที่มี dependency) ได้แต่ต้นก็ทำเสียเลย ไม่ต้องแยกโมดูลเหล่านั้นเป็นอีกฟังก์ชันในอาร์เรย์ แล้วค่อยใช้ __webpack_require__
เพื่อปลุกชีพขึ้นมาทำงานภายหลัง นั่นจึงเป็นเหตุให้ Scope Hoisting ช่วยทั้งลดขนาด Bundle ทั้งลดเวลาประมวลผลได้ในคราวเดียวกัน
จงทำ Code Spliting และใช้ Magic Comments
นั่งตัวตรง ขยับแว่น แล้วพิจารณาโค้ดต่อไปนี้
1// App.js2import React from 'react'3import { BrowserRouter as Router, Route } from 'react-router-dom'45import Home from './Home'6import About from './About'7import Contact from './Contact'89export default () => (10 <Router>11 <div>12 <Route path="/" exact component={Home} />13 <Route path="/about" component={About} />14 <Route path="/contact" component={Contact} />15 </div>16 </Router>17)
เราพบว่า App.js ต้องทำการโหลดโมดูลของ Home About และ Contact เข้ามาก่อนจึงจะสามารถทำงานได้อย่างถูกต้อง ลักษณะเช่นนี้เป็นผลให้ไฟล์ผลลัพธ์ของเราจะประกอบด้วยโค้ดของทั้ง 4 ไฟล์ที่กล่าวมา
เมื่อเราเข้าสู่ /
เราหวังว่าจะมีเฉพาะโค้ดของโมดูล App และ Home เท่านั้นที่ถูกโหลด ช่างโชคร้ายที่ Webpack กลั่นแกล้งให้ผลลัพธ์ที่ตอบกลับมีโค้ดของทั้ง 4 โมดูลมาเสนอหน้าพร้อมในเบราเซอร์ของเรา สถานการณ์เช่นนี้ Code Spliting คือทางออกของเรา
Code Spliting คือเทคนิคในการหั่นโค้ดของเราออกเป็นก้อนๆ (Chunk) เมื่อเบราเซอร์ต้องการแสดงผลส่วนไหน เราสามารถส่งเฉพาะ Chunk ที่เกี่ยวข้องกลับไปให้ได้ ด้วยเทคนิคนี้การแสดงผลของเราจะเร็วขึ้น เพราะมีเฉพาะโค้ดที่ต้องใช้จริงเท่านั้นที่ถูกโหลดไปจากเซิฟเวอร์ของเรา
เพราะเราแบ่งการทำงานของเราตามเส้นทาง (Route) ที่เกี่ยวข้อง เราจึงหั่นโค้ดของเราออกเป็นชิ้นๆได้ตามแต่ Route ที่เรามีอยู่ จากโปรแกรมข้างต้นเราจะได้ 4 chunks คือ chunk ของ entry module Home About และ Contact
เราสามารถทำ Code Spliting ได้โดยอาศัย Dynamic Import คู่กับไลบรารี่อย่าง react-loadable ดังนี้
1import React from 'react'2import { BrowserRouter as Router, Route } from 'react-router-dom'3import L from 'react-loadable'45const Loading = () => <div>Loading...</div>67const Loadable = (opts) =>8 L({9 loading: Loading,10 ...opts,11 })1213const AsyncHome = Loadable({14 // เมื่อเข้าสู่ / เราจะ dynamic import โมดูล Home เข้ามาใช้งาน15 // หาก path ปัจจุบันไม่ใช้ / โมดูล Home จะไม่ถูกโหลดมาทำงาน16 loader: () => import('./Home'),17})1819const AsyncAbout = Loadable({20 loader: () => import('./About'),21})2223const AsyncContact = Loadable({24 loader: () => import('./Contact'),25})2627export default () => (28 <Router>29 <div>30 <Route path="/" exact component={AsyncHome} />31 <Route path="/about" component={AsyncAbout} />32 <Route path="/contact" component={AsyncContact} />33 </div>34 </Router>35)
เมื่อทำการ Bundle เราจะเห็นข้อความดังนี้จาก Webpack
1Asset Size Chunks Chunk Names2 0.js.map 3.1 kB 0 [emitted]3 0.js 508 bytes 0 [emitted]4 2.js 509 bytes 2 [emitted]5 main.js 67 kB 3 [emitted] main6 1.js 511 bytes 1 [emitted]7 1.js.map 3.11 kB 1 [emitted]8 2.js.map 3.1 kB 2 [emitted]9 main.js.map 455 kB 3 [emitted] main10 index.html 324 bytes [emitted]
สังเกตได้ว่าตอนนี้เรามี chunk ทั้งหมด 4 chunks ด้วยกัน แต่มีเฉพาะ main.js ซึ่งเป็น entry เท่านั้นที่มีการแสดงชื่อของ chunk (Chunk Names) หากเราไม่ทำการเพิ่มชื่อให้ chunk อื่นๆ ข้อความแบบนี้ก็จะง่อยทันที เพราะเราคงตรัสรู้เองไม่ได้ว่า 0.js หรือ 1.js หมายถึงโมดูลไหนกันแน่
Webpack 3 มีสิ่งหนึ่งที่เรียกว่า Magic Comments อันเป็นกลุ่มของคอมเมนต์ที่ใส่ไปแล้วจะมีพลานุภาพพิเศษบางอย่าง สำหรับ dynamic import นั้นเราสามารถใส่ Magic Comments เพื่อให้ chunk มีชื่อได้ ดังนี้
1const AsyncHome = Loadable({2 loader: () => import(/* webpackChunkName: "home" */ './Home'),3})45const AsyncAbout = Loadable({6 loader: () => import(/* webpackChunkName: "about" */ './About'),7})89const AsyncContact = Loadable({10 loader: () => import(/* webpackChunkName: "contact" */ './Contact'),11})
ภายหลังการใส่ Magic Comments ที่ชื่อว่า webpackChunkName
ไปแล้ว chunk ของเราก็จะมีชื่อขึ้นมา
1Asset Size Chunks Chunk Names2 0.js.map 3.1 kB 0 [emitted] home3 0.js 508 bytes 0 [emitted] home4 2.js 509 bytes 2 [emitted] about5 main.js 67 kB 3 [emitted] main6 1.js 511 bytes 1 [emitted] contact7 1.js.map 3.11 kB 1 [emitted] contact8 2.js.map 3.1 kB 2 [emitted] about9 main.js.map 455 kB 3 [emitted] main10 index.html 324 bytes [emitted]
เมื่อเราทำ Code Spliting เป็นที่เรียบร้อย ครั้งถัดไปที่เราเข้าถึง path ใดๆ จะมีเพียง chunk ของ main และ chunk ที่สัมพันธ์กับ path นั้นเท่านั้นที่ถูกโหลด เช่น chunk about สำหรับ /about
จงโหลด Chunk แบบขนาน
พิจารณาโปรแกรมในหัวข้อก่อนหน้านี้
1export default () => (2 <Router>3 <div>4 <Route path="/" exact component={AsyncHome} />5 <Route path="/about" component={AsyncAbout} />6 <Route path="/contact" component={AsyncContact} />7 </div>8 </Router>9)
หากไฟล์ index.html ของเพื่อนๆเป็นเช่นนี้
1<!DOCTYPE html>2<html>3 <head>4 <meta http-equiv="Content-type" content="text/html; charset=utf-8" />5 <title>App</title>6 </head>7 <body>8 <div id="root"></div>9 <script10 type="text/javascript"11 src="./main-d47169f5a2acbbf383c0.js"12 ></script>13 </body>14</html>
เมื่อเราเข้า /
แน่นอนว่า main.js จะถูกโหลดขึ้นมาเป็นไฟล์แรก เมื่อ chunk ของ main.js ประมวลผลถึงส่วนของ Route จึงค้นพบว่า path ปัจจุบันเป็น /
จำเป็นต้องโหลด chunk ของ Home.js ขึ้นมาเพื่อเติมเต็มให้สมบูรณ์
สถานการณ์เช่นนี้เราพบว่า Home.js จะยังไม่ถูกโหลดมาแต่แรก หากแต่ต้องรอให้ chunk ของ main.js โหลดเสร็จเรียบร้อยและทำงานไประยะนึงก่อน ลักษณะการทำงานเช่นนี้จะมีดีเลย์ช่วงหนึ่งที่เกิดจากการต้องไปโหลด Home.js ขึ้นมาแสดงผล
เพื่อให้การทำงานราบลื่นขึ้น เราจึงควรโหลด chunk ที่เกี่ยวข้องในการแสดงผลเพจนั้นแบบขนานแต่แรก ด้วยการใส่เป็นแท็ก script ใน HTML ส่วนนี้อาจต้องอาศัยการทำ Server-Side Rendering ควบคู่ด้วย
1<!DOCTYPE html>2<html>3 <head>4 <meta http-equiv="Content-type" content="text/html; charset=utf-8" />5 <title>App</title>6 </head>7 <body>8 <div id="root"></div>9 <script10 type="text/javascript"11 src="./main-d47169f5a2acbbf383c0.js"12 ></script>13 <script14 type="text/javascript"15 src="./home-d47169f5a2acbbf383c0.js"16 ></script>17 </body>18</html>
จงใช้ Tree Shaking
Webpack1 ไม่สนับสนุน ES2015 Module ทำให้ทุกครั้งที่รวมไฟล์ (bundle) ต้องแปลงประโยค import/export เป็น require ใน CommonJS ก่อน ข้อเสียของวิธีนี้คือถ้าเรา export โมดูลหลายตัวแต่ไม่ได้ใช้งานมันเลย โมดูลเหล่านั้นก็ยังคงหลอกหลอนเราในไฟล์ผลลัพธ์ที่ได้จากการ bundle ด้วย
ไม่เป็นเช่นนั้นสำหรับ Webpack2 ขึ้นไปเนื่องจาก Webpack2+ สนับสนุน ES2015 Module ในตัวเองเลย ทำให้มันเข้าใจประโยค import/export โดยไม่ต้องแปลงเป็น CommonJS จังหวะที่มันรวมไฟล์เป็นหนึ่ง มันจึงสามารถใช้คุณสมบัติของ import/export ได้คือการกำจัดโมดูลส่วนเกินที่ไม่ใช้งาน เราเรียกอัลกอริทึมในการกำจัด dead code นี้ว่า Tree-shaking ดังนี้
1// myLib.js2export function util1() {}3export function util2() {}45// index.js6import { util1 } from './myLib.js'7util1()
จากโค๊ดข้างบนพบว่าเรา export util1 และ util2 จากโมดูล myLib แต่กลับ import แค่เพียง util1 เท่านั้น ทำให้ util2 เป็นของที่ไม่ได้ใช้งาน ด้วยความสามารถของ ES2015 Module มันจึงกำจัดส่วนเกินออกในไฟล์ bundle เหลือเพียงดังนี้
1function util1() {}2util1()
เมื่อไม่ export ทุกสรรพสิ่งรอบจักรวาลแบบที่เป็นใน CommonJS ดังนั้น Webpack2 จึงช่วยทำให้ไฟล์ bundle ของคุณผอมเพรียว เล็กกะจิดริดมากขึ้น
เพื่อให้ได้มาซึ่ง Tree Shaking คุณจำเป็นต้องปิดการแปลง Module ของ Babel ซะก่อน ด้วยการตั้งค่า module: false
1{2 "presets": [3 [4 "env",5 {6 "targets": {7 "browsers": ["last 2 versions"]8 },9 "modules": false << ตรงนี้10 }11 ],12 "react"13 ],14 "plugins": ["transform-object-rest-spread", "transform-class-properties"]15}
จงทำ Critical CSS
เป็นที่นิยมในการแปะ Stylesheet ของทั้งเว็บไซต์ไว้ในส่วน head
ของ HTML ดังนี้
1<html>2 <head>3 ...4 ...5 <link href="/dist/main-d47169f5a2acbbf383c0.css" media="screen, projection" rel="stylesheet" type="text/css" charset="UTF-8">6 </head>7 ...8 ...9 ...10</html>
โดยปกติเว็บบราวเซอร์จะอ่าน HTML แล้วสร้างเป็น DOM เช่นเดียวกันกับ CSS ที่จะอ่านแล้วสร้างเป็น CSSOM ในกระบวนการสร้างนี้จะบลอคการทำงานส่วนอื่นต่อไป นั่นหมายความว่าเมื่อเราแปะลิงก์ของ CSS ไว้ใน head เว็บบราวเซอร์ก็จะวิ่งบนเน็ตเวิร์กเพื่อไปดูดไฟล์ CSS มา จากนั้นจึงเข้ากระบวนการแปลงเป็น CSSOM ในจังหวะนี้เว็บบราวเซอร์ก็จะหมุนติ้วๆๆๆๆ จนกว่าจะทำงานเสร็จ ถึงจะเริ่มแสดงผลได้
ใช่แล้วหละฮะในจังหวะของการทำงานกับ CSS เนื้อหาของเราจะหมดสิทธิ์ในการแสดงผล ทำให้หน้าเพจของเราขาวโพลนเป็นหงอกบนหนังศีรษะเลยละ
เพราะทุกหน้าทีมีค่า เราจึงควรแปะ CSS ที่ใช้ในการแสดงผลแรกเริ่มเท่านั้นไว้ในส่วน head
หากเพจของเราเวลาโหลดเสร็จจะแสดงผลแค่ 30% ของจอ (อีก 70% ที่เหลือต้องสกอร์ถึงจะเห็น) เราก็ใส่ CSS ของ 30% นั้นไว้ใน head ส่วนอื่นที่เหลือก็เอาไว้ด้านล่างเพื่อไม่ให้บลอคการทำงานของส่วนอื่น และนี่หละครับคือหลักการทำงานของ Critical CSS (( อ่านเพิ่มเติม ล้วงลึกการจัดการเว็บให้แสดงผลเร็วขึ้นด้วย Critical CSS)
1<html>2 <head>3 ... ...4 <style>5 .article {6 ...;7 }89 .article__title {10 ...;11 }12 </style>13 </head>14 <body>15 ... ...16 <link17 href="/dist/main-d47169f5a2acbbf383c0.css"18 media="screen, projection"19 rel="stylesheet"20 type="text/css"21 charset="UTF-8"22 />23 </body>24</html>
ไม่ใช่เรื่องยากที่จะสร้าง Critical CSS ขึ้นมา แต่หากเพื่อนๆนั้นเกิดอาการขี้เกียจ ผมขอแนะนำ isomorphic-style-loader Loader ตัวนึงของ Webpack ที่จะช่วยเพื่อนๆทำ Critical CSS ได้อย่างง่ายดาย
นอกจากการใช้ isomorphic-style-loader แล้ว เรายังมีทางเลือกอื่นที่ดีกว่า เช่นการใช้ไลบรารี่ที่สนับสนุนการเขียน CSS ใน JavaScript พร้อมทำ Critical CSS ให้เสร็จสรรพอย่าง styled component หรือ emotion
จงตั้งค่า EnvironmentPlugin
React กับท่านผู้นำนั้นไม่ต่างกัน พี่แกจะไม่ยอมทุ่มด้วยโพเดี้ยมจนกว่าจะได้ด่าฉันใด React ก็จะไม่ยอมปล่อยให้เขียนโค้ดสะเปะสะปะด้วยการพ่น warning เตือนก่อนฉันนั้น เพราะ React มีการตรวจสอบและการแจ้งเตือนนักพัฒนาด้วยข้อความต่างๆมากมาย จึงเป็นประโยชน์ยิ่งนักต่อโหมด Development
ไม่ใช่สำหรับ Production ที่ไม่ต้องการให้ใครด่า ใครทุ่มโพเดี้ยมใส่ เราจึงต้องหาทางลบ warning หรือการตรวจสอบอะไรมากมายเหล่านั้นออกไปเสีย เพื่อให้ขนาดของ Bundle ของเราเล็กลง แต่ได้มาซึ่งประสิทธิภาพที่มากขึ้น
วิธีการที่ React ใช้ตรวจสอบก่อนเริ่มพ่น warning ทั้งหลายคือเช็คก่อนว่าเราทำงานบนสภาพแวดล้อมแบบ Production หรือไม่ ถ้าใช่ React จะไม่ทำการแจ้งเตือนใดๆ
1if (process.env.NODE_ENV !== 'production') {2 // ถ้าไม่ใช่ production ค่อยทำโค้ดข่างล่างนี้3}
เพราะเราต้องการสภาพการทำงานแบบ Production เราจึงต้องตั้งค่าให้ process.env.NODE_ENV
เป็น production
ทว่าเราไม่สามารถตั้งค่า NODE_ENV
ให้เป็น production ผ่าน shell (เช่น BASH) ได้ นั่นเพราะ process.env.NODE_ENV
ใน bundle จะหมายถึงตัวแปรที่ประกาศเพื่อใช้ใน bundle เท่านั้น เหตุนี้เราจึงต้องใช้ปลักอินที่ชื่อ DefinePlugin
เพื่อช่วยในการประกาศตัวแปรสำหรับ bundle
1new webpack.DefinePlugin({2 'process.env.NODE_ENV': JSON.stringify('production'),3})
หงุดหงิดใช่ไหม ที่ต้องคอยใส่ JSON.stringify
? เราแก้ได้ด้วย EnvironmentPlugin
ดังนี้
1new webpack.EnvironmentPlugin({2 NODE_ENV: 'production',3})
หลังจากการใช้ EnvironmentPlugin
Webpack จะทำการแทนที่ process.env.NODE_ENV
ด้วย production ให้เรา ดังนี้
1if ('production' !== 'production') {2 // ถ้าไม่ใช่ production ค่อยทำโค้ดข่างล่างนี้3}
แต่เดี๋ยวก่อน โค้ดใต้ if
อีกสิบชาติก็ไม่ถูกทำอยู่ดีเพราะ 'production' !== 'production'
แล้วเราจะเก็บโค้ดดุ้นนี้ไว้บูชาเรอะ! ถึงเวลาที่เราต้องกำจัดโค้ดที่ไม่ได้ใช้จริงออกไปด้วย UglifyJS แล้วหละ
1const UglifyJSPlugin = require('uglifyjs-webpack-plugin')23plugins: [4 new UglifyJSPlugin({5 compress: {6 warnings: false,7 },8 sourceMap: true,9 comments: false,10 minimize: false,11 }),12]
ถ้าเพื่อนๆได้ทดลองทำตามจะตาลุกวาวมากตอนนี้ เพราะขนาดไฟล์ผลลัพธ์จะลดลงโคตรๆหลังใช้ UglifyJS เชียว
แม้ EnvironmentPlugin
และ UglifyJS จะประเสริฐแค่ไหนก็ยังไม่พอจะสนองความขี้เกียจของโปรแกรมเมอร์ได้ Webpack นั้นรู้ใจจึงเตรียมชอตคัท -p
ที่ใส่ครั้งเดียวเปรี้ยวได้ทั้งสองงาน
1webpack -p
สิ่งที่ -p
ทำนั้นประกอบด้วยสองอย่างด้วยกัน คือ
--optimize-minimize
เพื่อการลดขนาดไฟล์ JavaScript ด้วยการใช้ UglifyJsPlugin และ LoaderOptionsPlugin--define process.env.NODE_ENV="production"
เช่นเดียวกับการตั้งค่า NODE_ENV ผ่าน EnvironmentPlugin
จบเลยแล้วกันยาวไปละ~
Preact คือทางเลือก
Preact นั้นเป็นไลบรารี่ที่ตั้งใจให้เหมือน React แต่ขนาดนั้นเบาหวิว เมื่อเราใช้ Preact ขนาดของ Bundle จึงลดฮวบจนน่าใจหาย เพื่อให้โค้ดเดิมของเราที่ใช้งานด้วย React ไม่ต้องเปลี่ยนแปลงมาก เราจึงใช้ preact-compat ควบคู่กับการตั้ง Alias ใน Webpack ดังนี้
1module.exports = {2 resolve: {3 alias: {4 react: 'preact-compat',5 'react-dom': 'preact-compat',6 },7 },8}
ไม่อยากจะบอกเลยว่า Preact ทำให้ขนาดของ Bundle ลดลงไปได้เยอะมากๆจริงๆ แต่ก็นั่นหละฮะคุณพร้อมยอมรับความเสี่ยงไหม? ใช้ React มาตลอดทั้ง development แต่ดันมาเป็น Preact ตอนทำ production? ถ้าอยากจะใช้ Preact นักเราก็ควรเริ่มต้นด้วย Preact ไปแต่ต้นซะเลย จะได้ไม่เจอปัญหาปวดตับเข้ากลางทาง แม้ชื่อจะบอกว่า Compatible แต่เอาเข้าจริงมันเข้ากันได้มากน้อยแค่ไหน ใครทราบ?
สรุป
มีหลายวิธีในการตั้งค่า Webpack เพื่อรีดประสิทธิภาพการทำงานกับ React ที่เรายังไม่ได้พูดถึงกันครับ ยังไงก็ขอเก็๋บไว้เป็นบทความหน้าจะดีกว่า สำหรับท่านใดที่ไม่อยากปวดตับและอยากมีดวงตาเห็นธรรมในชาตินี้ คอร์ส Core React: เรียนรู้การใช้งาน React อย่างมืออาชีพ ช่วยได้ฮะ # ซื้อโฆษณาช่วงนี้แล้นน
เอกสารอ้างอิง
Jeremias Menichelli (2017). Brief introduction to scope hoisting in Webpack. Retrieved July, 18, 2017, from https://medium.com/webpack/brief-introduction-to-scope-hoisting-in-webpack-8435084c171f
Webpack. EnvironmentPlugin. Retrieved July, 18, 2017, from https://webpack.js.org/plugins/environment-plugin/
Lucas Katayama (2016). Reducing Webpack bundle.js size. Retrieved July, 18, 2017, from https://www.slideshare.net/grgur/webpack-react-performance-in-16-steps
Nolan Lawson (2016). The cost of small modules. Retrieved July, 18, 2017, from https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/
Grgur Grisogono (2016). Webpack & React Performance in 16+ Steps. Retrieved July, 18, 2017, from https://www.slideshare.net/grgur/webpack-react-performance-in-16-steps
สารบัญ
- จงใช้ Scope Hoisting
- จงทำ Code Spliting และใช้ Magic Comments
- จงโหลด Chunk แบบขนาน
- จงใช้ Tree Shaking
- จงทำ Critical CSS
- จงตั้งค่า EnvironmentPlugin
- Preact คือทางเลือก
- สรุป
- เอกสารอ้างอิง