ล้วงลึกการจัดการเว็บให้แสดงผลเร็วขึ้นด้วย Critical CSS

Nuttavut Thongjor

เรื่องมันเริ่มมาจากลองทดสอบเว็บด้วย PageSpeed Insights ของลุง Google แล้วคุณลุงให้คะแนนผมไม่เต็มร้อย

PageSpeed

สิ่งแรกที่มองเห็นชัดเลยคือ นี่เราไม่ได้ใช้ CDN ใช่ไหม ดูด CSS ตรงจากเว็บหลักเลยนะเนี่ย ปัญหาถัดมาคือนี่ CSS ของเราทำไมมีแค่ main? จริงๆควรแยก CSS ของ third-party ออกเป็นอีกไฟล์นะจะได้ปล่อยให้ติด cache ยาวๆ แต่ปัญหาของเรื่องนี้ไม่ใช่ CDN หรือ cache ครับ หากเป็นเรื่องของ Critical CSS ต่างหาก

ปัญหาของการแปะลิงก์ CSS ใน head

แอบแกะ HTML ของเว็บตัวเองดูดีกว่า หน้าตาของมันก็ประมาณนี้ครับ

HTML
1<html>
2 <head>
3 ... ...
4 <link
5 href="/dist/main-d47169f5a2acbbf383c0.css"
6 media="screen, projection"
7 rel="stylesheet"
8 type="text/css"
9 charset="UTF-8"
10 />
11 </head>
12 ... ... ...
13</html>

ผมแปะลิงก์ของ CSS ไว้ใน head เลย มันก็ดูปกติใช่ไหมครับ แต่เพราะความปกติของมันนี่หละที่ทำให้เว็บผมเสียคะแนนในส่วนนี้

โดยปกติเว็บบราวเซอร์จะอ่าน HTML แล้วสร้างเป็น DOM เช่นเดียวกันกับ CSS ที่จะอ่านแล้วสร้างเป็น CSSOM ในกระบวนการสร้างนี้จะบลอคการทำงานส่วนอื่นต่อไป นั่นหมายความว่าเมื่อเราแปะลิงก์ของ CSS ไว้ใน head เว็บบราวเซอร์ก็จะวิ่งบนเน็ตเวิร์กเพื่อไปดูดไฟล์ CSS มา จากนั้นจึงเข้ากระบวนการแปลงเป็น CSSOM ในจังหวะนี้เว็บบราวเซอร์ก็จะหมุนติ้วๆๆๆๆ จนกว่าจะทำงานเสร็จ ถึงจะเริ่มแสดงผลได้

งั้นเอาใหม่ เมื่อการนำลิงก์ CSS ไปแปะที่หัวเป็นกรรมหนัก เราย้ายมันไปไว้หลังใน body แบบนี้ได้ไหม?

HTML
1<html>
2 <head>
3 ... ...
4 </head>
5 <body>
6 ... ... ...
7 <link
8 href="/dist/main-d47169f5a2acbbf383c0.css"
9 media="screen, projection"
10 rel="stylesheet"
11 type="text/css"
12 charset="UTF-8"
13 />
14 </body>
15</html>

เย้ๆ แค่นี้เว็บของเราก็ไม่ถูกบลอคแล้ว แสดงผลได้ทันทีเลย แต่ช้าก่อน! ผลลัพธ์ที่คุณได้คือ หน้าเพจของคุณจะเหมือนสาวแก้ผ้าไปแปปนึง เนื่องจาก HTML แสดงผลบนหน้าเพจแล้ว แต่ยังไม่ได้นำสไตล์ไปใส่ แบบนี้

FOUC

มันจะเป็นเช่นนี้จนกว่า CSS ของคุณจะได้รับการโหลดและแสดงผล ถึงจะกลับมาเป็นเว็บสวยงามเช่นเดิม เราเรียกสภาพที่หน้าเว็บล่อนจ้อนไร้ผ้าผ่อนแบบนี้ว่า Flash of Unstyled Content หรือ FOUC

เราควรจะใส่ CSS ตำแหน่งไหนของเพจดี?

จะใส่ลิงก์ของ CSS ใน head ก็แสดงผลช้า ครั้นจะเอาไปแปะไว้ท้ายสุดของ body ก็เป็น FOUC มองทางไหนก็ไม่ดีทั้งนั้น

เรามีหลักการที่เรียกว่า Progressive Rendering ความหมายคือ ทุกๆวินาทีมีค่า อย่าให้ผู้ใช้งานต้องแหกตาดูเพจเปล่า อะไรที่แสดงผลได้ก็ค่อยแสดงผลออกมา การปล่อยให้ผู้ใช้งานนั่งดูเพจเปล่าจนสิ้นสุดกระบวนการถึงค่อยแสดงผลนั้นเป็นกรรมหนัก ลองดูภาพประกอบที่ผมไปจิ๊กมาจาก Google ข้างล่างครับ

Progressive Rendering

ปกป้องการบลอคการแสดงผลด้วย media queries

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

HTML
1<link href="style.css" rel="stylesheet" />
2<link href="print.css" rel="stylesheet" media="print" />
3<link href="other.css" rel="stylesheet" media="(min-width: 40em)" />
  • ตัวบนสุดไม่มีเงื่อนไขอะไรเลย มันบลอคแน่นอน
  • ตัวกลางมีเงื่อนไขบอกว่าใช้สำหรับการ print ฉะนั้นแล้วมันจะได้รับการนำมาใช้เมื่อเราสั่งแสดงผล print ไม่บลอคเพจทันทีที่โหลด
  • กรณีสุดท้าย ถ้าในขณะที่เปิดเพจ บราวเซอร์ของเรามีความกว้างขนาดมากกว่าหรือเท่ากับ 40em CSS ตัวนี้ก็จะบลอคเพจแล้วนำสไตล์ไปใช้งาน

เห็นไหมครับลิงก์ CSS แต่ละแบบมีผลไม่เหมือนกัน แต่ถ้าจะให้เรามานั่งกำหนดตามขนาดหน้าจอแบบนี้ มันไม่ได้แก้ปัญหาสิ่งที่เป็นอยู่เลยแม้แต่น้อย เพราะฉะนั้น ผ่านครับ!

ทำไมไม่โหลดขนานแบบ asynchronous หละ?

ผู้อ่านบางคนอาจคิดว่า เราควรโหลดลิงก์ CSS แบบ asynchronous ไปเลยคือไม่ต้องให้หน้าเพจโดนบลอคด้วยการรอลิงก์ CSS ประมวลผล แต่ให้โหลดขนานไปกับการแสดงผลเพจ เมื่อ CSS พร้อมเมื่อไหร่ค่อยให้ไปปรากฎบนหน้าเพจของเรา

ฟังดูดีใช่ไหมครับ แต่ปัญหาของเราก็จะเหมือนเดิม คือถ้าผู้ใช้งานเข้าหน้าเพจแล้ว CSS ยังไม่โหลดมาแสดงผล หน้าเพจของเราก็จะแก้ผ้าแบบ FOUC เช่นเดิม ดังนั้นแล้วเราควรโหลด CSS แบบ asynchronous ควบคู่ไปกับการทำ critical CSS ดังจะกล่าวต่อไป

แก้ปัญหาด้วย Critical CSS Rendering Path

ในทุกๆไฟล์ CSS ที่เราโหลดเข้ามาเชื่อไหมครับว่าเราไม่ได้ใช้ทุกๆสไตล์ที่อยู่ในไฟล์ ไฟล์ CSS นั้นอาจประกอบด้วย CSS ของ Twitter Bootstrap font-awesome หรือสิ่งอื่นใด ทั้งนี้มีเพียงบางส่วนของสไตล์เท่านั้นที่เราใช้กับหน้าเพจที่ผู้ใช้งานร้องขอ คำถามคือ เมื่อเป็นเช่นนี้ทำไมเราต้องให้ผู้ใช้งานรอ CSS ที่ไม่ได้ใช้ในเพจนั้นประมวลผลด้วย?

ใช่ครับ เวลาเป็นเงินเป็นทอง ฉะนั้นแล้วเราจึงแงะเอาสไตล์ที่ใช้จริงในเพจนั้นออกมาจากก้อน CSS ที่อุดมไปด้วยของที่ไม่ได้ใช้ เจ้าก้อน CSS ที่เอาออกมานี้เป็นส่วนที่น้อยที่สุดที่ใช้แสดงผลแล้วเพจดูโอเค จากนั้นเราเอาก้อน CSS นี้มาใส่ไว้ใน head ดังข้างล่าง เราเรียกเจ้าก้อน 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>

เราได้อะไรจากการทำเช่นนี้บ้าง? อย่างแรกคือเรารู้ว่า .article และ .article__title จำเป็นสำหรับการแสดงผล ถ้าไม่มีสิ่งนี้แล้วหน้าเพจของเราจะเละหรือเป็น FOUC เราจึงเอาสองสิ่งนี้ไปใส่ที่ head เพื่อให้โหลดและประมวลผลก่อนแสดงเพจ เมื่อเพจแสดงขึ้นมาจึงสวยงามตามพิมพ์นิยม ขอให้สังเกตนะครับว่าเรายังแปะลิงก์ CSS ไว้ใน body อยู่

เราได้ CSS ที่จำเป็นต่อการแสดงผลแล้วหนิ ทำไมยังต้องการ CSS เต็มๆจากลิงก์อีก? นั่นเป็นเพราะ critical CSS เป็นเพียง CSS ที่จำเป็นต่อการแสดงผลเท่านั้น ไม่ใช่ทั้งหมดที่เราต้องการ เช่น เราจะไม่เอา font-face ที่ใช้กำหนดฟ้อนต์มาเป็น critical CSS ทั้งนี้เพราะแม้ไม่มีฟ้อนต์เว็บเรา layout ก็ไม่เสีย เมื่อ <link...> ได้รับการโหลดจึงค่อยประยุกต์ใช้ฟ้อนต์ที่เราต้องการเข้าไปทีหลังก็ยังไม่สาย


*คำถามชวนคิด : ทำไมเราต้องนำ Critical CSS ไปแปะไว้ในส่วนหัวด้วย <style> ทั้งๆที่การทำแบบนี้เป็นการเพิ่มขนาด HTML ของเราให้ใหญ่ขึ้น? ทำไมเราจึงไม่รวม Critical CSS ไว้ในไฟล์ CSS ไฟล์หนึ่งแล้วนำไปแปะใน header เป็น <link ...>?


แล้วอะไรหละที่ควรเป็นและไม่ควรเป็น critical CSS?

  • ของที่เอาออกแล้ว layout ไม่เสีย ไม่ควรเป็น critical CSS เช่น font-face

  • จินตนาการถึงอุปกรณ์ของผู้ใช้งาน เมื่อเขาเข้าเพจของคุณ เขาเห็นส่วนไหนเป็นสิ่งแรกสิ่งนั้นคือ critical CSS เนื่องจากส่วนนั้นสำคัญต่อการเห็นครั้งแรก ส่วนที่เหลือของหน้าเพจผู้ใช้งานต้องสกอร์ลงไป ถึงจะเห็นเนื้อหาซึ่งจังหวะที่สกอร์ลงไปนั้น CSS ตัวหลักทั้งไฟล์คงโหลดและประมวลผลไปหมดแล้ว จึงไม่มีความจำเป็นใดๆต้องให้สไตล์ของ element ที่อยู่ด้านล่างเพจเป็น critical CSS เราเรียกส่วนของเพจที่แสดงผลโดยไม่ต้อง scroll ว่า above-the-fold content

Area

ทฤษฎีมันกินไม่ได้ มาลงมือปฏิบัติกัน!

บทความนี้เราจะใช้ library ตัวนึง ไอ้หน้ายิ้มนี่หละครับชื่อ critical

Critical

เอาหละลองสร้างโฟลเดอร์เปล่าขึ้นมาก่อน สร้างไฟล์ package.json ขึ้นมาพร้อมใส่เนื้อหาดังนี้

Code
1{
2 "name": "critical-css",
3 "version": "1.0.0",
4 "description": "Critical CSS",
5 "main": "index.js",
6 "scripts": {
7 "start": "node index.js"
8 },
9 "dependencies": {},
10 "devDependencies": {
11 "critical": "^0.7.2"
12 },
13 "author": "Nuttavut Thongjor",
14 "license": "ISC"
15}

จากนั้นก็ลุยคำสั่งนี้เลยครับ

Code
1npm install

เมื่อเสร็จเรียบร้อย เราก็ไปแงะ HTML ที่เราต้องการจะแกะ critical CSS มาใส่ไว้ในไฟล์ชื่อ index.html พร้อมด้วยก้อน CSS ฉบับเต็มใส่ไว้ในไฟล์ชื่อ index.css

เมื่อทุกอย่างพร้อม เราสร้างไฟล์ index.js เพื่อเรียกใช้งาน critical ดังนี้

Code
1var critical = require('critical')
2
3critical.generate({
4 // เป็นการบอก critical ว่าหลังจากแกะ critical CSS ออกมาแล้ว
5 // ให้นำก้อน CSS ที่ได้ไปแปะลง <style>...</style>
6 // ในส่วน head หรือที่เราเรียกกันว่า inline style
7 inline: true,
8 src: 'index.html',
9 css: ['main.css'],
10 // ต้องการให้ผลลัพธ์ที่ได้อยู่ในไฟล์ไหน
11 dest: 'index-critical.html',
12 // Viewport หรือขนาดของบราวเซอร์ที่ใช้
13 width: 1300,
14 height: 900,
15 // ลดขนาดไฟล์ด้วยการทำ minify
16 minify: true,
17 // อย่างที่กล่าวข้างต้น font-face ไม่ควรเป็น critical CSS
18 ignore: ['font-face'],
19})

เอาหละ ถึงเวลาลุยกันล้าววว สั่ง npm start ได้เลย ผลลัพธ์ที่ได้เป็นดังนี้ครับ

HTML
1<!DOCTYPE html>
2<html lang="en-us" ...>
3 <head>
4 ...
5 <style type="text/css">
6 ._1F0AZhBxFhwVd0r-GLZMiI{height:20rem;z-index:3;color:#fff;position:relative;display:flex;...
7 </style>
8 <script id="loadcss">
9 ;(function (u, s) {
10 !(function (e) {
11 'use strict'
12 var n = function (n, t, o) {
13 var l,
14 r = e.document,
15 i = r.createElement('link')
16 if (t) l = t
17 else {
18 var a = (r.body || r.getElementsByTagName('head')[0]).childNodes
19 l = a[a.length - 1]
20 }
21 var d = r.styleSheets
22 ;(i.rel = 'stylesheet'),
23 (i.href = n),
24 (i.media = 'only x'),
25 l.parentNode.insertBefore(i, t ? l : l.nextSibling)
26 var f = function (e) {
27 for (var n = i.href, t = d.length; t--; )
28 if (d[t].href === n) return e()
29 setTimeout(function () {
30 f(e)
31 })
32 }
33 return (
34 (i.onloadcssdefined = f),
35 f(function () {
36 i.media = o || 'all'
37 }),
38 i
39 )
40 }
41 'undefined' != typeof module ? (module.exports = n) : (e.loadCSS = n)
42 })('undefined' != typeof global ? global : this)
43 for (var i in u) {
44 loadCSS(u[i], s)
45 }
46 })(
47 ['/dist/main-d47169f5a2acbbf383c0.css'],
48 document.getElementById('loadcss')
49 )
50 </script>
51 <link
52 href="/dist/main-d47169f5a2acbbf383c0.css"
53 media="screen, projection"
54 rel="stylesheet"
55 type="text/css"
56 charset="UTF-8"
57 />
58 </head>
59</html>

ความจริงแล้วผลลัพธ์ที่ได้นั้นยาวติดกันเป็นพรืดนะครับเพราะเราทำ minify แต่อันนี้ผมจัดแท็บให้ดูอ่านง่ายหน่อย

จะพบว่าเจ้า library ตัวนี้แกะ critical CSS ของเราแล้วนำไปใส่ <style> ข้างบนตามที่เราคาดหวัง แต่เหนือสิ่งอื่นใดเครื่องมือตัวนี้สร้างการโหลด CSS หลักของเราให้เป็นแบบ asynchronous ด้วย คือโหลดพร้อมไม่ต้องรอบลอค สังเกตตรง <script id="loadcss"> บรรทัดที่ 8 ครับ

แบบนี้มันดู manual เกินไป มีดีกว่านี้ไหม?

ผู้อ่านบางคนอาจคิดว่า ทำไมฉันต้องมานั่งก็อบปี้ HTML และ CSS มาวางไว้ก่อน ถึงค่อยสั่งเจ้าเครื่องมือนี้สร้าง critical CSS ด้วย อย่างนี้ถ้าฉันมีซัก 10 เพจไม่ต้องมานั่งก็อบ วางซักสิบรอบหรอ

จริงครับ ฉะนั้นแล้วเราควรจะจัดการมันได้ดีกว่านี้ เช่น ใช้ Gulp เข้ามาช่วยในการสร้าง critical CSS เมื่อเราทำการ build project เป็นต้น สำหรับผู้อ่านที่สนใจการใช้งานกับ Gulp สามารถเข้าไปดูเพิ่มเติมได้ใน Github ครับ

แต่เราใช้ Webpack นะนาย? ผมมีเขียนเกี่ยวกับการทำ critical CSS สำหรับ Isomorphic Application ด้วย React และ React ในชุดบทความ สอนสร้าง Isomorphic Application ด้วย React.js และ Redux ใน 5 วัน ติดตามได้นะครับ

เป็นอย่างไรกันบ้างครับ คอนเซ็ปต์ critical CSS ดูดีใช่ไหม มันช่วยปรับปรุงให้การแสดงผลของเว็บคุณดีขึ้น นั่นจึงเป็นการเพิ่ม UX หรือ user experience นั่นเอง สำหรับผมแล้วคงต้องหาเวลากลับไปปรับปรุงให้ babelcoder.com รองรับ critical CSS ด้วยแล้วหละ แล้วจะกลับมารายงานผลให้ฟังครับ

เอกสารอ้างอิง

Google Developers Logo. Critical rendering path. Retrieved May, 12, 2016, from https://developers.google.com/web/fundamentals/performance/critical-rendering-path/

addyosmani. Extract & Inline Critical-path CSS in HTML pages. Retrieved May, 12, 2016, from https://github.com/addyosmani/critical

Google Developers Logo. Render blocking CSS. Retrieved May, 12, 2016, from https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-blocking-css?hl=en

Critical Rendering Path. Critical rendering path . Retrieved May, 12, 2016, from https://developers.google.com/web/fundamentals/performance/critical-rendering-path/

Dean Hume (2015). Understanding Critical CSS. Retrieved May, 12, 2016, from https://www.smashingmagazine.com/2015/08/understanding-critical-css/

สารบัญ

สารบัญ

  • ปัญหาของการแปะลิงก์ CSS ใน head
  • เราควรจะใส่ CSS ตำแหน่งไหนของเพจดี?
  • ปกป้องการบลอคการแสดงผลด้วย media queries
  • ทำไมไม่โหลดขนานแบบ asynchronous หละ?
  • แก้ปัญหาด้วย Critical CSS Rendering Path
  • แล้วอะไรหละที่ควรเป็นและไม่ควรเป็น critical CSS?
  • ทฤษฎีมันกินไม่ได้ มาลงมือปฏิบัติกัน!
  • แบบนี้มันดู manual เกินไป มีดีกว่านี้ไหม?
  • เอกสารอ้างอิง