รีดประสิทธิภาพโค้ด React ด้วย Webpack ตอนที่ 1

Nuttavut Thongjor

คุณคือนักพัฒนา 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 ของแต่ละโมดูลด้วยเช่นกัน

JavaScript
1// App.js
2// แต่ละไฟล์ทำการ export ฟังก์ชัน
3import module1 from './module1'
4import module2 from './module2'
5import module3 from './module3'
6
7console.log(module1())
8console.log(module2())
9console.log(module3())

จากโปรแกรมข้างต้นเราพบว่า App.js ไม่สามารถจบการทำงานได้ด้วยตนเอง สามโมดูลที่ App.js ต้องการคือ module1 module2 และ module3 ต้องถูกโหลดเข้ามาในฐานะที่เป็น Dependency ของ App.js โค้ดของเราจึงจะทำงานได้อย่างถูกต้อง หลังจากการ Bundle ของ Webpack เราจะได้ผลลัพธ์หน้าตาออกมาประมาณนี้

JavaScript
1;(function (modules) {
2 function __webpack_require__(moduleId) {
3 // โหลดโมดูล
4 }
5
6 // ทำการโหลด entry module
7 // หาก App.js ของเราเป็น entry module และอยู่ในลำดับ 0
8 // โมดูล 0 จะถูกโหลด
9 return __webpack_require__((__webpack_require__.s = 0))
10})([
11 /* module 0 */
12 function (module, __webpack_exports__, __webpack_require__) {
13 // โค้ดของ App.js
14 },
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 นั้นเป็นเทคนิคที่จะย้ายการประกาศโมดูลไปไว้ตอนต้นของฟังก์ชัน ทำให้เราสามารถเข้าถึงโมดูลเหล่านี้ในภายหลังได้อย่างง่ายได้

JavaScript
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 ก็เป็นอันเรียบร้อย

JavaScript
1module.exports = {
2 plugins: [new webpack.optimize.ModuleConcatenationPlugin()],
3}

หลังจากการ Bundle ด้วยฟีเจอร์ของ ModuleConcatenationPlugin เราจะได้ผลลัพธ์ใหม่ดังนี้

JavaScript
1(function(modules) {
2 function __webpack_require__(moduleId) {
3 // โหลดโมดูล
4 }
5
6 // ทำการโหลด entry module
7 // หาก App.js ของเราเป็น entry module และอยู่ในลำดับ 0
8 // โมดูล 0 จะถูกโหลด
9 return __webpack_require__(__webpack_require__.s = 0);
10})([
11 /* module 0 */
12 (function(module, __webpack_exports__, __webpack_require__) {
13 // โค้ดของ App.js
14 var module1_defaultExport = (function() { // ... })
15 var module2_defaultExport = (function() { // ... })
16 var module3_defaultExport = (function() { // ... })
17
18 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

นั่งตัวตรง ขยับแว่น แล้วพิจารณาโค้ดต่อไปนี้

JavaScript
1// App.js
2import React from 'react'
3import { BrowserRouter as Router, Route } from 'react-router-dom'
4
5import Home from './Home'
6import About from './About'
7import Contact from './Contact'
8
9export 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 ดังนี้

JavaScript
1import React from 'react'
2import { BrowserRouter as Router, Route } from 'react-router-dom'
3import L from 'react-loadable'
4
5const Loading = () => <div>Loading...</div>
6
7const Loadable = (opts) =>
8 L({
9 loading: Loading,
10 ...opts,
11 })
12
13const AsyncHome = Loadable({
14 // เมื่อเข้าสู่ / เราจะ dynamic import โมดูล Home เข้ามาใช้งาน
15 // หาก path ปัจจุบันไม่ใช้ / โมดูล Home จะไม่ถูกโหลดมาทำงาน
16 loader: () => import('./Home'),
17})
18
19const AsyncAbout = Loadable({
20 loader: () => import('./About'),
21})
22
23const AsyncContact = Loadable({
24 loader: () => import('./Contact'),
25})
26
27export 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

Code
1Asset Size Chunks Chunk Names
2 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] main
6 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] main
10 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 มีชื่อได้ ดังนี้

JavaScript
1const AsyncHome = Loadable({
2 loader: () => import(/* webpackChunkName: "home" */ './Home'),
3})
4
5const AsyncAbout = Loadable({
6 loader: () => import(/* webpackChunkName: "about" */ './About'),
7})
8
9const AsyncContact = Loadable({
10 loader: () => import(/* webpackChunkName: "contact" */ './Contact'),
11})

ภายหลังการใส่ Magic Comments ที่ชื่อว่า webpackChunkName ไปแล้ว chunk ของเราก็จะมีชื่อขึ้นมา

Code
1Asset Size Chunks Chunk Names
2 0.js.map 3.1 kB 0 [emitted] home
3 0.js 508 bytes 0 [emitted] home
4 2.js 509 bytes 2 [emitted] about
5 main.js 67 kB 3 [emitted] main
6 1.js 511 bytes 1 [emitted] contact
7 1.js.map 3.11 kB 1 [emitted] contact
8 2.js.map 3.1 kB 2 [emitted] about
9 main.js.map 455 kB 3 [emitted] main
10 index.html 324 bytes [emitted]

เมื่อเราทำ Code Spliting เป็นที่เรียบร้อย ครั้งถัดไปที่เราเข้าถึง path ใดๆ จะมีเพียง chunk ของ main และ chunk ที่สัมพันธ์กับ path นั้นเท่านั้นที่ถูกโหลด เช่น chunk about สำหรับ /about

จงโหลด Chunk แบบขนาน

พิจารณาโปรแกรมในหัวข้อก่อนหน้านี้

JavaScript
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 ของเพื่อนๆเป็นเช่นนี้

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 <script
10 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 ควบคู่ด้วย

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 <script
10 type="text/javascript"
11 src="./main-d47169f5a2acbbf383c0.js"
12 ></script>
13 <script
14 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 ดังนี้

JavaScript
1// myLib.js
2export function util1() {}
3export function util2() {}
4
5// index.js
6import { util1 } from './myLib.js'
7util1()

จากโค๊ดข้างบนพบว่าเรา export util1 และ util2 จากโมดูล myLib แต่กลับ import แค่เพียง util1 เท่านั้น ทำให้ util2 เป็นของที่ไม่ได้ใช้งาน ด้วยความสามารถของ ES2015 Module มันจึงกำจัดส่วนเกินออกในไฟล์ bundle เหลือเพียงดังนี้

JavaScript
1function util1() {}
2util1()

เมื่อไม่ export ทุกสรรพสิ่งรอบจักรวาลแบบที่เป็นใน CommonJS ดังนั้น Webpack2 จึงช่วยทำให้ไฟล์ bundle ของคุณผอมเพรียว เล็กกะจิดริดมากขึ้น

เพื่อให้ได้มาซึ่ง Tree Shaking คุณจำเป็นต้องปิดการแปลง Module ของ Babel ซะก่อน ด้วยการตั้งค่า module: false

Code
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 ดังนี้

JavaScript
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)

HTML
1<html>
2 <head>
3 ... ...
4 <style>
5 .article {
6 ...;
7 }
8
9 .article__title {
10 ...;
11 }
12 </style>
13 </head>
14 <body>
15 ... ...
16 <link
17 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 จะไม่ทำการแจ้งเตือนใดๆ

JavaScript
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

JavaScript
1new webpack.DefinePlugin({
2 'process.env.NODE_ENV': JSON.stringify('production'),
3})

หงุดหงิดใช่ไหม ที่ต้องคอยใส่ JSON.stringify ? เราแก้ได้ด้วย EnvironmentPlugin ดังนี้

JavaScript
1new webpack.EnvironmentPlugin({
2 NODE_ENV: 'production',
3})

หลังจากการใช้ EnvironmentPlugin Webpack จะทำการแทนที่ process.env.NODE_ENV ด้วย production ให้เรา ดังนี้

JavaScript
1if ('production' !== 'production') {
2 // ถ้าไม่ใช่ production ค่อยทำโค้ดข่างล่างนี้
3}

แต่เดี๋ยวก่อน โค้ดใต้ if อีกสิบชาติก็ไม่ถูกทำอยู่ดีเพราะ 'production' !== 'production' แล้วเราจะเก็บโค้ดดุ้นนี้ไว้บูชาเรอะ! ถึงเวลาที่เราต้องกำจัดโค้ดที่ไม่ได้ใช้จริงออกไปด้วย UglifyJS แล้วหละ

JavaScript
1const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
2
3plugins: [
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 ที่ใส่ครั้งเดียวเปรี้ยวได้ทั้งสองงาน

Code
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 ดังนี้

JavaScript
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 คือทางเลือก
  • สรุป
  • เอกสารอ้างอิง