網站效能有感優化 💫
隨著公司產品、服務內容及流量的增加,網站效能優化的需求開始浮現;為了提高網站的效能和使用者體驗,甚至是幫助 SEO。身為前端開發人員,來分享與團隊一起實現有感優化的經驗吧!
以下我將這次整個網頁效能優化專案的落地過程分為三部分和大家分享,歩驟包括問題分析、優化策略及執行成果等方面,希望對各位有所幫助,如果在讀完本篇有得到一點點的啟發或有實際的應用,歡迎在文後按讚及分享!
問題分析
網站效能優化的目的除了要有好的使用者體驗之外,最終目標還是要讓新的使用者找得到你的網站(除了打廣告以外),所以在 Google Search 依舊是市佔第一的情況下,使用 PageSpeed Insights 這個工具來幫忙分析網站的設計及效能問題。
最佳化建議
PageSpeed Insights 會列出網站效能的問題,並給出一些改善的建議。當然如果有時間可以對所有建議慢慢修復,但考慮到專案有一定的時程,我們優先採納有助於提升網頁載入速度的部分。以下是此次使分數提高的主要修復:
- 提供 next-gen 格式的圖片
使用 WebP 和 AVIF 等圖片格式的壓縮效果通常比 PNG 或 JPEG 要好,這代表下載速度更快,數據用量更少。 - 延後載入畫面外圖片
建議在所有要資源載入完成之前,延遲載入畫面外圖片和隱藏項目,以縮短互動準備時間。 - 圖片編碼有效率
經過最佳化的圖片載入速度較快,且能節省使用者的行動數據用量。 - 避免耗用大量網路資源
大量的網路酬載會增加使用者的費用負擔,而且往往會延長網頁載入時間。 - 將主要執行緒的工作降到最低
建議你縮短剖析、編譯及執行 JavaScript 所耗費的時間。提供較小的 JavaScript 酬載可能會有幫助。 - 減少 JavaScript 執行時間
建議你縮短剖析、編譯及執行 JavaScript 所耗費的時間。提供較小的 JavaScript 酬載可能會有幫助。
優化策略
前端技術日新月異,優化方法很多,甚至各式框架替代方案輩出。每每看著新東西,就想把網站砍掉重練。不過還是得認清現實啦,面對跨國電商網站每月滿檔活動,時間、人力資源都需要納入考量。所幸 Tomofun 支持技術團隊不斷優化甚至創新,工程師的技術展現,讓網站效能提升,乃至於提升使用者體驗,都是公司所重視的。
考量現況,我們決定採用漸進式優化,先針對高流量、作為門面的首頁,以降低最受影響的 TTI、SI 數值為目標,逐一將所有頁面調整完成。
圖片格式優化
核心在於以較少時間迅速給使用者最適合的圖片。
- 圖片轉檔成 WebP、AVIF
- 壓縮圖片
- 調整成適當尺寸
可以請設計師提供對應圖檔,或者用設計工具自行導出。而我們利用 Prismic* 內容管理系統基於 next/image
擴充的元件自動實現,其內建圖片處理服務(imgix)負責轉檔、壓縮及產出不同尺寸圖片,next/image
則是為 <img />
添加 sizes
、srcset
等屬性,好讓不同裝置顯示適當圖片。
/* before */
...
return (
<>
<img src={image.url} alt={image.alt} />
{/* or */}
<div style={{ backgroundImage: image.url }} />
</>
)
...
/* after */
import { PrismicNextImage } from '@prismic/next'
...
return (
<>
<PrismicNextImage field={image} />
{/* or */}
<PrismicNextImage field={image} layout='fill' objectFit='cover' />
</>
)
...
關於next/image
的功用節錄自 Next.js Image Optimization:
Some of the optimizations built into the Image component include:
Improved Performance: Always serve correctly sized image for each device, using modern image formats
Visual Stability: Prevent Cumulative Layout Shift automatically
Faster Page Loads: Images are only loaded when they enter the viewport, with optional blur-up placeholders
Asset Flexibility: On-demand image resizing, even for images stored on remote servers
資源載入優化
延後載入還不需互動(尚未在 user viewport 中)的資源,如:圖片、影片等,以減少資源請求與等待時間。
next/image
圖片一樣使用 next/image
,實現 lazy loading。
<video /> + Intersection Observer
- 設定 video
preload='none'
取消影片預載入 - 若有自動播放需求,使用 Intersection Observer API,偵測影片進入 viewport 再播放
// Intersection Observer API 3rd party library
import useInView from 'react-cool-inview'
const Video = ({ autoPlay, preload = 'none', ...restProps }) => {
const ref = useRef()
const { observe } = useInView({
threshold: VISIBLE_OFFSET,
onEnter: () => { ref.current.play() },
onLeave: () => { ref.current.pause() },
})
return (
<video
preload={preload}
{...restProps}
ref={(el) => {
if (autoPlay) { observe(el) }
ref.current = el
}}
/>
)
}
JavaScript 優化
下載 script 與運行都需要時間,透過動態載入調整下載順序,以及簡化或延後部分運行時間,以加快初始網頁載入速度。
減少 above the fold(首屏) runtime
- 移除入場動畫
- 延遲載入 viewport 以外區塊
以最快速度顯示 above the fold,減少使用者等待感。以我們網站為例,Hero 即是產品形象圖與產品頁連結。最好的情況是使用者直接點了去產品頁,進行購買流程;除此之外,沒互動到的區塊,基本上不必載入。
import dynamic from 'next/dynamic'
import useInView from 'react-cool-inview'
const Skeleton = () => <div style={{ height: '500px' }} />
const OtherSection = dynamic(
() => import('components/OtherSection'), // 其他區塊為動態載入
{ loading: () => <Skeleton /> } // 載入前放置相同空白區塊,避免 CLS
)
export default function Page() {
const [shouldSectionVisible, setShouldSectionVisible] = useState(false)
const { observe } = useInView({
rootMargin: '-20px 0px 0px 0px',
unobserveOnEnter: true,
onEnter: () => { setShouldSectionVisible(true) }
})
return (
<>
<Hero />
{shouldSectionVisible
? <OtherSection />
: <Skeleton ref={observe} />} {/* 放置空白區塊 */}
</>
)
}
減少 JS 樣式
CSS 支援越來越多新潮功能,很多樣式不需要出動 JS 即可實現,有些甚至換一個角度想,也有機會減少 JS 樣式。例如:
/* before */
...
const [image, setImage] = useState({ url: mobileImageURL })
useEffect(() => {
// 根據不同螢幕寬度,顯示不同圖片
if (window.innerWidth > MOBILE_BREAKPOINT) {
setImage({ url: desktopImageURL })
}
}, [])
return (
<div style={{ backgroundImage: image.url }} />
)
...
/* after */
// style.css
@media (max-width: 600px) {
.mobile-hidden {
display: none;
}
}
@media (min-width: 601px) {
.desktop-hidden {
display: none;
}
}
// index.js
...
return (
<>
<div className='desktop-hidden' style={{ backgroundImage: mobileImageURL }} />
<div className='mobile-hidden' style={{ backgroundImage: desktopImageURL }} />
</>
)
...
更新 Library
部分 library 在迭代過程中也持續優化,例如:減少 bundle size、runtime 時間,所以將一些 library 更新後,網站也能一同受惠。這方法看似被動,但在這個求新求變的領域,是個值得嘗試的選擇!
目前我們只升級 CMS 相關、有減少 bundle size 的 library,其他有待日後探索。
Per-Page Layouts
- 利於各頁獨立設定 layout 元件
- 利於逐頁升級、替換元件與 library
最後將過去於 Next.js* 框架中的 Single Shared Layout 改成 Per-Page Layouts,使首頁與其餘頁面,能同時存在兩套以上不盡相同的全局設定,給漸進式優化一個助攻,可以實現逐頁 migration。
// components/Layout.js
import { Provider } from 'a-library'
export default function Layout({ children }) {
return (
<Provider>
{/* Your content */}
{children}
</Provider>
)
}
// components/LayoutV2.js
import { ProviderV2 } from '@upgraded/library' // 更新的 library
export default function LayoutV2({ variant, children }) {
return (
<ProviderV2>
{/* Your content */}
{children}
</ProviderV2>
)
}
// pages/about.js (work in process)
import { ComponentA } from 'a-library'
import Layout from 'components/Layout'
export default function Page() {
return (
/** Your content */
<ComponentA />
)
}
Page.getLayout = function getLayout(page) {
return (
<Layout>{page}</Layout>
)
}
// pages/index.js (optimized)
import { ComponentA } from '@upgraded/library'
import LayoutV2 from 'components/LayoutV2' // 引入新版 layout
export default function Page() {
return (
/** Your content */
<ComponentA />
)
}
Page.getLayout = function getLayout(page) {
return (
<LayoutV2 variant='home'>{page}</LayoutV2> // 也能為 layout 添加變化
)
}
執行成果
實際執行後,我們可以看到網站載入速度顯著提升,幾乎是快了一倍,明顯的改善使用者體驗。
而 TTI 與 SI 數值也是有不錯的成績,順利達成此次優化目標。
結論
- 依照 PageSpeed Insights 指示分析問題
- 考量網站營運現況,採取漸進式優化
- 越快顯示內容給使用者,相對越有好的效能與體驗
前端優化百百種,不外乎就是讓瀏覽器少做一些事(回歸原始)。我們持續探索更多的優化策略和技術,讓網站與團隊更加進步!
* Frontend Tech Stack
+-----------+-------------+
| UI | React |
+-----------+-------------+
| Framework | Next.js |
+-----------+-------------+
| State | Redux |
+-----------+-------------+
| Style | Material UI |
+-----------+-------------+
| CMS | Prismic |
+-----------+-------------+