วิธีลดขนาด bundle size ของ Tailwind

Nuttavut Thongjor

Tailwind CSS นั้นใช้หลักการของ Atomic CSS ที่หนึ่งคลาสมีความหมายเดียว เช่น คลาส text-center มีความหมายเดียวกับการใช้ text-align: center

ปัญหาของการระบุคลาสในลักษณะนี้ให้กับแต่ละอีลีเมนต์คือชื่อคลาสที่ใช้มีขนาดค่อนข้างยาวและซ้ำไปซ้ำมา ในทุกครั้งที่เราเรียกใช้คลาสเหล่านี้ในอีลีเมนต์อื่น ๆ

Button Group

จากรูปกลุ่มของปุ่มข้างต้น ถ้าเขียนเป็นคลาสผ่าน Tailwind CSS จะได้โค้ดดังนี้

HTML
1<div class="m-1 space-x-1">
2 <button class="py-1 px-2 bg-green-400 text-white rounded">Pfizer</button>
3 <button class="py-1 px-2 bg-green-400 text-white rounded">Moderna</button>
4 <button class="py-1 px-2 bg-green-400 text-white rounded">Astrazeneca</button>
5 <button class="py-1 px-2 bg-red-400 text-white rounded">Sinovac</button>
6</div>

เราจะมีวิธีไหนบ้างที่สามารถลดขนาดชื่อคลาสเพื่อทำให้ผลลัพธ์สุดท้ายของโค้ดเรามีขนาด bundle size ที่ลดลง?

Mangle CSS Class

จากโค้ดของปุ่มข้างต้นพบว่าโค้ดของเรามีหลายคลาสที่ใช้ซ้ำกันโดยแต่ละคลาสมีชื่อค่อนข้างยาวเสียด้วย เช่น bg-green-400 และ text-white เป็นต้น

จะดีกว่าไหมถ้าเราสามารถหาวิธีย่อชื่อของแต่ละคลาสลงได้ เมื่อชื่อของคลาสสั้นลงขนาดโค้ดเราก็จะลดลงตาม

เราสามารถทำสิ่งนี้ให้เกิดขึ้นจริงได้บน Tailwind CSS ด้วยการใช้ mangle-css-class-webpack-plugin ที่เป็น Plugin ของ Webpack

ต่อไปนี้เป็นขั้นตอนการใช้งาน Plugin ดังกล่าวกับ React (ที่สร้างผ่าน Create React App)

  1. ติดตั้ง Plugin นี้ผ่านคำสั่งคือ
Code
1yarn add -D mangle-css-class-webpack-plugin
  1. ภายหลังจากการสร้างโปรเจคบน Create React App แล้วให้ทำการติดตั้ง Craco ผ่านคำสั่งคือ
Code
1yarn add -D @craco/craco
  1. ทำการสร้างไฟล์ craco.config.js เพื่อตั้งค่าการใช้งาน Webpack ในที่นี้เราจะทำการลดขนาดของชื่อคลาสแต่ละตัวของ Tailwind ให้สั้นลงเฉพาะการทำงานบน Production (หลัง build โปรเจคแล้ว) ส่วนบน development เราจะไม่ดำเนินการใด ๆ
Code
1// craco.config.js
2const { whenProd } = require('@craco/craco')
3const MangleCssClassPlugin = require('mangle-css-class-webpack-plugin')
4
5const twRegEx =
6 '(([a-zA-Z-:]*)[\\\\\\\\]*:)*([\\\\\\\\]*!)?tw-[a-zA-Z-]([a-zA-Z0-9-]*([\\\\\\\\]*(\\%|\\#|\\.|\\[|\\]))*)*'
7
8module.exports = {
9 style: {
10 postcss: {
11 plugins: [require('tailwindcss'), require('autoprefixer')],
12 },
13 },
14 webpack: {
15 plugins: {
16 add: [
17 ...whenProd(
18 () => [
19 new MangleCssClassPlugin({
20 classNameRegExp: twRegEx,
21 log: true,
22 }),
23 ],
24 []
25 ),
26 ],
27 },
28 },
29}

จากนั้นทำการอัพเดตส่วนของ scripts ใน package.json ดังนี้

Code
1{
2 "scripts": {
3 "start": "craco start",
4 "build": "craco build",
5 "test": "craco test",
6 "eject": "react-scripts eject"
7 }
8}

การทำงานของ MangleCssClassPlugin เราต้องทำการระบุว่าให้ตัว Plugin จัดการลดขนาดคลาสที่มีรูปแบบใด ให้เราทำการระบุรูปแบบคลาสของ Tailwind ลงไป ฉะนั้นแล้วคลาสรูปแบบอื่นที่ไม่ใช่ของ Tailwind จะไม่ได้รับผลกระทบ

RegEx ตามบรรทัดที่ 5 นั้นจะเข้าได้กับคลาสของ Tailwind โดยแต่ละคลาสเราจะต้องเติม tw- เข้าไปนำหน้าเสียก่อนถึงใช้งานได้

  1. เพื่อให้ Tailwind CSS เข้าใจว่าแต่ละคลาสที่เคยใช้ได้บนระบบ ตอนนี้จะต้องเติม tw- เสียก่อนเพื่อให้กลับมาใช้ได้อีกครั้ง เราต้องทำการระบุค่า prefix ในไฟล์ตั้งค่าของ Tailwind คือ tailwind.config.js
Code
1module.exports = {
2 purge: [],
3 darkMode: false,
4 prefix: 'tw-',
5 theme: {
6 extend: {},
7 },
8 variants: {
9 extend: {},
10 },
11 plugins: [],
12}
  1. ตอนนี้รูปแบบของโค้ดจะต้องเปลี่ยนไปด้วยการเติม tw- ให้แต่ละคลาสของ Tailwind โค้ดล่างนี้เป็นการใช้งานกับ React
JSX
1<div className="tw-m-1 tw-space-x-1">
2 <button className="tw-py-1 tw-px-2 tw-bg-green-400 tw-text-white tw-rounded">
3 Pfizer
4 </button>
5 <button className="tw-py-1 tw-px-2 tw-bg-green-400 tw-text-white tw-rounded">
6 Moderna
7 </button>
8 <button className="tw-py-1 tw-px-2 tw-bg-green-400 tw-text-white tw-rounded">
9 Astrazeneca
10 </button>
11 <button className="tw-py-1 tw-px-2 tw-bg-red-400 tw-text-white tw-rounded">
12 Sinovac
13 </button>
14</div>
  1. ทำการ build โปรเจคด้วยการออกคำสั่ง yarn build ขั้นตอนนี้จะเห็นจาก log ว่า Plugin ของเราได้ทำการเปลี่ยนชื่อคลาสของ Tailwind ให้เป็นชื่อที่สั้นลงแล้ว

Minify Class Names

เมื่อเราทำการเปิดเว็บเพื่อเข้าถึงโค้ดที่ผ่านการ build แล้ว จะได้คลาสใหม่ที่สั้นลงส่งผลให้ bundle size เล็กลงตามด้วย

Reduce Class Name Size

หลีกเลี่ยงการสร้างคลาสใหม่ เปลี่ยนไปสร้างคอมโพแนนท์แทน

กรณีที่เราใช้คลาสกับกลุ่มของปุ่มดังต่อไปนี้

JSX
1<>
2 <button className="tw-py-1 tw-px-2 tw-bg-green-400 tw-text-white tw-rounded">
3 Pfizer
4 </button>
5 <button className="tw-py-1 tw-px-2 tw-bg-green-400 tw-text-white tw-rounded">
6 Moderna
7 </button>
8 <button className="tw-py-1 tw-px-2 tw-bg-green-400 tw-text-white tw-rounded">
9 Astrazeneca
10 </button>
11</>

พบว่าโค้ดของเราจะอ่านยากและยาวเพราะเราใช้ชื่อคลาสซ้ำกันเยอะ บางคนอาจแก้ไขปัญหานี้ด้วยการสร้างคลาสใหม่

CSS
1.btn {
2 @apply tw-py-1 tw-px-2 tw-bg-green-400 tw-text-white tw-rounded;
3}

ตัวอย่างข้างต้นเป็นการสร้างคลาสใหม่ชื่อ btn โดยมีการแสดงผลตามแต่ค่าของคลาสใน Tailwind ที่ถูกระบุหลัง @apply วิธีเรียกใช้งานใหม่คือ

JSX
1<>
2 <button className="btn">Pfizer</button>
3 <button className="btn">Moderna</button>
4 <button className="btn">Astrazeneca</button>
5</>

แม้ว่าวิธีนี้จะทำให้โค้ดอ่านง่ายขึ้น แต่ก็ทำให้สูญเสียคุณสมบัติความเป็น Atomic CSS ด้วยเช่นกัน นอกจากนี้คลาสใหม่คือ btn จะทำให้ขนาด bundle size ของ CSS เพิ่มขึ้นด้วย เนื่องจาก Tailwind จะคัดลอกค่าของแต่ละคลาสที่ใช้ในการสร้าง btn มาผนวกเป็นค่าของคลาส btn เสียเอง

CSS
1/* ค่าของ btn ที่ได้จากการใช้ @apply */
2.btn {
3 border-radius: 0.25rem;
4 --tw-bg-opacity: 1;
5 background-color: rgba(52, 211, 153, var(--tw-bg-opacity));
6 padding-left: 0.5rem;
7 padding-right: 0.5rem;
8 padding-top: 0.25rem;
9 padding-bottom: 0.25rem;
10 --tw-text-opacity: 1;
11 color: rgba(255, 255, 255, var(--tw-text-opacity));
12}

วิธีที่ดีกว่าคือการสร้างคอมโพแนนท์ขึ้นมาใหม่แล้วทำการเรียกใช้คอมโพแนนท์นั้นซ้ำในทุกครั้งที่ต้องการสร้างปุ่ม

JSX
1const Button: FC = ({ children }) => {
2 return (
3 <button className="tw-py-1 tw-px-2 tw-bg-green-400 tw-text-white tw-rounded">
4 {children}
5 </button>
6 )
7}
8
9// เรียกใช้ปุ่ม
10;<>
11 <Buttin>Pfizer</Buttin>
12 <Buttin>Moderna</Buttin>
13 <Buttin>Astrazeneca</Buttin>
14</>

ลบคลาสที่ไม่ได้ใช้

ไม่ใช่ทุกคลาสของ Tailwind CSS ที่โปรเจคเราจะได้ใช้ ดังนั้นเราจึงควรลบคลาสที่ไม่ได้ใช้ออกจากผลลัพธ์สุดท้ายของเรา เราสามารถบอก Tailwind ให้ตรวจสอบคลาสจากไฟล์ index.html และไฟล์นามสกุล js, jsx, ts และ tsx หาก Tailwind ไม่พบคลาสใดในไฟล์เหล่านั้น Tailwind จะไม่ทำการรวมคลาสเหล่านั้นในผลลัพธ์ของ CSS ด้วยเมื่อเรา build โปรเจค เราสามารถทำสิ่งนี้ได้ด้วยการระบุค่าใน purge ของการตั้งค่าในไฟล์ tailwind.config.js

Code
1module.exports = {
2 purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
3 darkMode: false,
4 prefix: 'tw-',
5 theme: {
6 extend: {},
7 },
8 variants: {
9 extend: {},
10 },
11 plugins: [],
12}

อย่าสร้างการใช้งานคลาสของ Tailwind แบบ Dynamic

สมมติปุ่มของเราสามารถระบุค่าสีได้สองแบบคือ success สำหรับพื้นหลังสีเขียว และ danger สำหรับพื้นหลังสีแดง (กรณีไม่รุค่าสีเริ่มต้นเป็น success) โค้ดการเรียกใช้งานเป็นดังนี้

JSX
1<div className="tw-m-1 tw-space-x-1">
2 <Button>Pfizer</Button>
3 <Button>Moderna</Button>
4 <Button>Astrazeneca</Button>
5 <Button color="danger">Sinovac</Button>
6</div>

เราอาจใช้พร็อพเพอร์ตี้ color เป็นเงื่อนไขในการพิจารณาเลือกค่าสีมาแสดงผลเป็นพื้นหลังดังโค้ดบรรทัดที่ 6

JSX
1type ButtonProps = {
2 color?: 'success' | 'danger',
3}
4
5const Button: FC<ButtonProps> = ({ color = 'success', children }) => {
6 const bgColor = `tw-bg-${color === 'success' ? 'green' : 'red'}-400`
7
8 return (
9 <button className={`tw-py-1 tw-px-2 ${bgColor} tw-text-white tw-rounded`}>
10 {children}
11 </button>
12 )
13}

การสร้างคลาสโดยใช้ส่วนหนึ่งส่วนใดของพร็อพเพอร์ตี้มาต่อข้อความเพื่อเรียกใช้คลาสของ Tailwind นั้นจะทำให้คำสั่ง purge ของ Tailwind ทำงานได้ไม่ถูกต้อง วิธีที่ดีกว่าคือการสร้างคลาสแบบตายตัวขึ้นมาตามค่าพร็อพเพอร์ตี้แทน ดังนี้

JSX
1const bgColor = color === 'success' ? 'tw-bg-green-400' : 'tw-bg-red-400'
สารบัญ

สารบัญ

  • Mangle CSS Class
  • หลีกเลี่ยงการสร้างคลาสใหม่ เปลี่ยนไปสร้างคอมโพแนนท์แทน
  • ลบคลาสที่ไม่ได้ใช้
  • อย่าสร้างการใช้งานคลาสของ Tailwind แบบ Dynamic