Optimize web performance

Thứ năm, ngày 15 tháng 6 năm 2023

Mình đã optimize web performance như thế nào?

Trang web được load và hiển thị như thế nào?

  1. Browser load HTML file (block)
  2. Parsing HTML (non block)
  3. Load các tài nguyên khác như JS, CSS, ảnh, font,.... (non block)
  4. Render (loop)
    • Calculate style, layout và painting các element (block)
    • Thực thi codeJS (block)
Quá trình hiển thị một trang web trên trình duyệt

Người dùng cảm nhận một trang web nhanh nếu tất cả các quá trình trên nhanh nhất có thể. Hiệu năng của các quá trình trước ảnh hưởng trực tiếp tới toàn bộ quá trình sau.

Web performance

Để đánh giá hiệu năng của một trang web, Google đưa ra 6 chỉ số ảnh hưởng đến hiệu năng và trải nghiệm người dùng (user centric metrics)

  • Largest Contentful Paint
  • First Contentful Paint
  • Cumulative Layout Shift
  • Total Blocking TIme
  • Time To Interactive
  • Speed Index

6 chỉ số này là kết quả của các quá trình load và render page ở phần trên, nên để optimize, ta phải nắm được chi tiết cách một trang web được render như thế nào.

Largest Contentful Paint

Thời gian mà phần tử lớn nhất trên màn hình được hiển thị. Thời gian này càng ngắn, người dùng sẽ cảm thấy trang web dường như khá nhanh mặc dù các nội dùng khác có thể chưa đựọc render.

Ví dụ: thời gian banner hiển thị được lấy làm điểm LCP cho trang homepage của phongvu.vn

Phần tử LCP trên trang phongvu.vn

Một số lưu ý:

  • với các phần tử LCP, nên được load nhanh nhất khi có thể
  • LCP thường là các phần từ img hoặc các phần tử p
  • LCP nên được bố cục một cách rõ ràng trên trang web tại thời điểm load, tránh các phần tử nhỏ, làm cho việc khó xác định LCP

First Contentful Paint

Thời gian để browser có thể render nội dung ra màn hình, FCP luôn là thời điểm trước FCP nên nếu FCP nhỏ thì LCP sẽ nhỏ theo. FCP ảnh hưởng bởi các blocking resource như CSS, font nên việc tối ưu CSS và load font sẽ làm giảm FCP.

Cumulative Layout Shift

CLS là tình trạng các phần tử trong màn hình thay đổi vị trí và kích thước trong thời gian tải trang. Nếu CLS cao sẽ dẫn tới trải nghiệm không tốt về UI như ví dụ sau

Total blocking time

Thông số ảnh hưởng do CPU phải thực thi các tác vụ trên main thread quá dài như:

  • JS parsing
  • HTML parsing
  • calculate style
  • layout
  • JS executing

Nếu thời gian này cao có thể gây ra một số vấn đề sau:

  • giảm frame rate, mỗi frame nên được update và render trong khoảng 16.6ms để đảm bảo 60fps
  • thời gian user có thể tương tác được với web dài sẽ mang lại trải nghiệm không tốt

Một số cách để làm giảm thời gian này:

  • giảm code JS browser phải thực thi khi load trang
  • tránh việc style lại các element như insert thẻ style, thay đổi các attribute liên quan đến vị trí, kích thước của các element (recalculate style, layout thrashing)

Time To Interactive

Thời gian người dùng có thể tương tác được với trang web, thời gian này càng ngắn, người dùng sẽ cảm giác trang web đang chạy nhanh. Thông số này bị ảnh hưởng bởi việc CPU bị các tác vụ khác chiếm quá nhiều nên để tối ưu chỉ số này cần giảm các tác vụ dùng CPU đến mức tối thiểu. Các tác vụ đó đã được nói đến ở phần Total blocking time.

Speed Index

Thời gian hiển để hiển thị nội dung trang web. Cũng như các phần khác, thời gian này bị ảnh hưởng bởi CPU bị chiếm do các tác vụ khác hoặc do các block resource như font, css. Để giảm thời gian này, cần giảm thời gian CPU bị chiếm cũng như load các block resource nên được load nhanh nhất có thể.

Cheetsheet

Cumulative Layout Shift

  • Ý nghĩa:
    • Mức độ các phần tử thay đổi kích thước và vị trí khi load trang
  • Yếu tố ảnh hưởng:
    • thời gian load các resource chậm như font
    • các phần tử chưa được render không được xác đinh 1 placeholder cụ thể
  • Phương pháp tối ưu:
    • xác định trước 1 placeholder cụ thể cho các phần tử, đặc biệt là img. Với img, nên set thuộc tính width, height rõ ràng.
    • trong react, với các component SSR, nên khởi tạo useState với server data nếu state đó tham gia vào quá trình render. Ví dụ thay vì list = useState([]) thì list = useState(serverList)
    • SSR

First Contentful Paint

  • Ý nghĩa:
    • Thời gian để browser băt đầu painting lên màn hình
  • Yếu tố ảnh hưởng:
    • blocking resource như css, font
  • Phương pháp tối ưu:
    • tối ưư việc load các blocking resource
    • SSR các phần từ được hiển thị trong viewport trong loading time

Largest Contentful Paint

  • Ý nghĩa:
    • Thời gian hiển thị phần tử lớn nhất trên viewport trong loading time, thường là các phần từ img hoặc các text element như p
  • Yếu tốt ảnh hưởng:
    • với các text element: thời gian load font
    • với các img element: thời gian load image
  • Phương pháp tối ưu:
    • tối thiểu hóa việc load font, không nên load font dư thừa
    • với image, nên preload một số ảnh đóng vai trò là lcp
    • không thực hiện việc hiển thị LCP element bằng code JS hay CSS (background-image), sử dụng thẻ img
    • nên server side rendering (SSR) với các LCP element
    • các component LCP không nên lazy render

Speed Index

  • Ý nghĩa:
    • Thời gian trang web có thể hiển thị nội dung
  • Yếu tố ảnh hưởng:
    • blocking resource
    • main thread blocking time
  • Phương pháp tối ưu:
    • giảm thời gian xử lí các yếu tố ảnh hưởng theo các phương thức ở trên

Time to interactive

  • Ý nghĩa:
    • Thời gian người dùng có thể tương tác được với trang web
  • Yếu tố ảnh hưởng:
    • main thread blocking time
  • Phương pháp tối ưu:
    • giảm thời gian main thread bị block bằng các phương pháp ở trên
    • không nên lạm dụng idle time (sử dụng requestIdleCallback quá nhiều), thời gian idle cũng là khoảng thời gian browser chờ để xử lí user input

Total blocking time

  • Ý nghĩa:
    • Thời gian main thread bị block
  • Yếu tố ảnh hưởng:
    • JS parsing
    • HTML parsing
    • calculate style
    • layout
    • JS executing
  • Phương pháp tối ưu:
    • treeshaking: https://web.dev/reduce-javascript-payloads-with-tree-shaking
    • giảm lượng HTML trả về do SSR
    • tránh việc load quá nhiều style hoặc lazy load style - recalculate style
    • tránh việc truy cập/chỉnh sửa các thuộc tính về vị trí và kích thước của các phần tử - layout thrashing

Một số vấn đề cần lưu ý

Treeshaking

Tree shaking là phương pháp loại bỏ các deadcode ra khỏi bundle, làm giảm kích thước file JS trả về client. Phương pháp này được hỗ trợ bởi các bunudler như webpack, esbuild, rollup,...

Một số chú ý về tree shaking:

  • tree shaking chỉ thực hiện ở application, không thực hiện ở library
  • để giúp các bundler có thể tree shake, nên consider set trường sideEffects trong package.json với giá trị phù hợp
  • chỉ nên tách function ra shared package nếu chúng được sử dụng ít nhất 2 lần trở lên
  • tránh việc import * from ... để bundler có thể tree shake
  • tree shaking chỉ thực hiện được trong ESM, các bundler không hỗ trợ tree shaking trên CommonJS, do đó, hạn chế tối đa sử dụng CommonJS cho các ứng dụng trên browser
  • với các ứng dụng hybrid như NextJS, code trên server rất dễ lẫn với code trên client, do đó cần lưu ý và sử dụng các bundle anaylzer như webpack-bundle-analyzer để theo dõi
  • các resource nhưng image nên được tách ra file và sử dụng CDN để serve, tránh việc bundle cả ảnh vào trong JS bundle

Đọc thêm: https://blog.theodo.com/2021/04/library-tree-shaking

Lazy import, lazy load

lazy import được sử dụng để giảm bundle size. Thay vì bundle code của tất cả tính năng, chúng ta chỉ nên import code của những tính năng cần thiết.

Ví dụ: code cho popup thông báo lỗi chỉ được load về khi popup cần hiện lên khi có lỗi xảy ra. Tại thời điểm load trang, phần code đó không được bundle vào JS, giúp thời gian load giảm.

Một số lưu ý:

  • không lazy load những đoạn code làm ảnh hưởng đến LCP, CLS
  • lazy import không làm ảnh hưởng tới code trên server
  • tránh lạm dụng lazy import không cần thiết, điều này có thể tạo ra những file JS quá nhỏ, giảm khả năng nén của web server, ảnh hưởng tới hiệu năng chung
  • sử dụng lazy loading của HTML như img

Lazy hydrate

Lazy hydrate tránh việc code được thực thi không cần thiết.

Ví dụ: chỉ render các component trong viewport (nhất là tại loading time) sẽ làm giảm thời gian CPU bị blocking. Kết hợp với IntersectionObserver, chúng chỉ nên được render khi ở trong viewport.

Do việc detect một component có ở trong viewport hay không do JS thực thi nên những phần từ LCP cũng không nên được render bằng cách lazy hydrate này.

Time to first byte

Tuy không phải là 1 user centric metric nhưng TTFB lại rất quan trọng trong web performance do nó ảnh hưởng tới tất cả các chỉ số web performance. Trong tất cả các TTFB của các resource, TTFB của file HTML là quan trong nhất vì nó là entrypoint của 1 webpage. Tối ưu chỉ số này sẽ làm tăng điểm số chung đáng kể.

Để improve chỉ số này, có 1 vài phương pháp như:

  • caching
  • giảm thời gian SSR bằng cách loại bỏ các component không cần thiết

Optimize bandwidth

Tại thời điểm loading, giảm bandwidth giúp các resource quan trọng được load nhanh hơn. trang web sẽ được hiển thị lên màn hình nhanh nhất có thể.

Lưu ý:

  • tránh load các thirdparty không quan trọng tại thời điểm này như tracking hay các script phục vụ marketing
  • tránh preload quá nhiều resource
  • chỉ nên đặc các resource quan trọng, cần load nhanh nhất có thể trong thẻ head của html, chuyển các script không quan trọng xuống cuối thẻ body
  • Ưu tiên xử lí HTML tĩnh trước các resource như JS hay các image chưa được hiển thị trên trang web

Chuyện gì đã xảy ra trước ECOM-338?

Note: ECOM-338 là Epic tối ưu web performance cho phongvu.vn

Điểm tổng quan do google pagespeed khá thấp trên cả desktop và mobile.

Sử dụng lighthouse hoặc google pagespeed, chúng ta có được các đầu mối để có thể investigate nguyên nhân và cách tối ưu.

Trước ECOM-338, có một số vấn đề sau:

  • Thời gian CPU bị block quá lớn: ảnh hưởng đến trực tiếp đến hiệu năng quả trang web (Smoothness)
  • Cumulative Layout Shift cao: các phần từ trên màn hình thay đổi vị trí và kích thước liên tục dẫn tới trải nghiệm về UI không tốt (Visual stability)
  • Time To Interactive cao: làm tăng thời gian người dùng có thể tương tác được với trang web (Runtime responsiveness)
  • Trên mobile, các chỉ số như Largest Contentful Paint và First Contentful Paint cao: làm giảm khả năng trải nghiệm của người dùng (Meaningful)
Desktop
Mobile

Làm thế nào để tìm ra nguyên nhân?

Có nhiều công cụ giúp tìm hiểu vấn đề và gợi ý hướng giả quyết cho các vấn đề về hiệu năng của website.

Lighthouse và google pagespeed

Công cụ này sẽ phân tích và đưa ra điểm số tổng quan trong trang web. Nó sẽ đo lường 6 user centric metric về hiệu năng web, đây là những đầu mối quan trọng để tiến hành tối ưu về sau. Nó cũng đưa ra một điểm số tổng quan để chúng ta có thể theo dõi tiến độ tối ưu. Ngoài ra, nó cũng đưa ra những vấn đề gặp phải và gợi ý hướng giải quyết. Theo mình, công cụ này nên được sử dụng trước mỗi lần tối ưu và sau khi tối ưu, nó sẽ giúp theo dõi được tiến độ cũng như đưa ra hướng đi tiếp theo cần phải làm.

Để sử dụng Lighthouse, mở Chrome Dev Tools bằng cách nhấn Ctrl+Shift+I và chọn tab Lighthouse

Chúng ta có thể đo lường trang web với các khía cạnh khác nhau như Hiệu năng (Performance), SEO, ... Tuy nhiên, trong bài này chỉ tập trung tới phần hiệu năng nên các khía cạnh khác sẽ được uncheck. Ngoài ra, Lighthouse có thể đo lường trên cả thiết bị desktop và mobile. Với từng thiết bị, Lighthouse sẽ thiết lập các chỉ số tương đương như throtting network thành mạng 3G cho các thiết bị mobile hay làm quá trình xử lí tác vụ chậm đi (throtting CPU).

Sau khi chọn xong các tùy chỉnh, click vào Analyze page load. Lưu ý, trong quá trình Lighthouse đang phân tích, tránh thực hiện các tác vụ khác để làm giảm sai sót trong quá trình phân tích.

Từ lighthouse, chúng ta có thể nhận thấy một vài vấn đề trong project shop-front như sau:

  • Lượng code JS được bundle vào output dư thừa
  • Thời gian thực thi code JS quá dài, chiếm nhiều CPU làm giảm khả năng render trang web
  • Lựa chọ thư viện chưa hợp lí dẫn tới hiệu năng giảm trên các thiết bị mobile
  • Không tuân thủ các best practice về web performance
  • Thời gian phản hồi của server không cao làm ảnh hưởng toàn bộ các chỉ số khác trong đánh giá

Performance insight

Để mở công cụ này, vào Chrome Dev Tools > Performance Insight

Performance Insight

Công cụ này giúp chúng ta cái nhìn chi tiết hơn về trang web theo thời gian. Nó sẽ cho ta biết các vấn đề, user centric metric xảy ra như thế nào theo thời gian, từ đó giúp ta dễ dàng biết được nên tối ưu ở đâu, cho chỉ số nào.

Nếu như Lighthouse cho chúng ta biết được các vấn đề là gì, chỉ số cao hay thấp thì Performance Insight cho chúng ta biết được chúng có quan hệ như thế nào. Khi chúng ta có nhận thức chỗ cần tối ưu, bước tiếp theo là tối ưu như thế nào? Công cụ tiếp theo sẽ cho ta biết điều đó.

Performance

Để sử dụng công cụ này, mở Chrome Dev Tools > Performance

Performance in Google Chrome

Có 4 phần chúng ta cần lưu ý:

  1. Biểu đồ này cho chúng ta cái nhìn tổng quan về khối lượng công việc mà browser phải làm khi load trang web. Các màu trong biểu đồ ứng với các loại công việc (chi tiết ở phần 4).
  2. Khối này cho chúng ta biết chi tiết resource và thứ tự được tải về khi load trang (Network)
  3. Chi tiết các function/task đươc thực thi trong main thread, chúng cho ta biết thứ tự thực thi và quan hệ giữa chúng.
  4. Giống khối 3 nhưng các thông tin đó được hiển thị theo các cách khác nhau như Summary, Bottom-Up, Call Tree, Event Log

Webpack bundle analyzer

Bundle có thể được phân tích bằng webpack-bundle-analyzer, với công cụ này, một số issue có thể được phát hiện như:

  • code server được bundle lẫn với client
  • duplicate module
  • bundle bị thừa hoặc các thư viện đang sử dụng commonjs → không thể tree shaking

Kết quả sau khi optimize

Điểm số đã đạt được khá cao khi đo trên mobile là 60 và desktop là 80-90

Tuy nhiên còn một vấn đề khó và ảnh hưởng rất lớn đến kết quả đó là script do người quản lí website nhúng vào. Đó là những đoạn mã có thể phá bỏ mọi kết quả tối ưu mà mình đã không lường trước do thiếu kinh nghiệm trong quá trình tìm hiểu

Google Tag Manager (GTM) cho phép nhúng các đoạn mã JS, CSS, HTML vào trang web một cách linh động.

Điểm số cho phiên bản desktop

Không nhúng GTM
Nhúng GTM

Điểm số cho phiên bản mobile

Không nhúng GTM
Nhúng GTM

Ngoài ra, điểm số còn phụ thuộc khá nhiều vào những yếu tố bên ngoài như thời gian trả kết quả của API, network người dùng, cấu hình máy tính,...