# 玩轉 Web Vitals By Yahoo Engineer **Paul Li** (Laravel x Vue Conf Taiwan 2022 ) ## Web Vitals 2020 年 5 月,Google 開啟了一項名為 Web Vitals 使用者體驗優化計劃,為網站使用體驗提供統一的評估標準。在該計畫底下, Google 發展了一套名為網站使用體驗的核心指標(Core Web Vitals),並由 3 項子指標所構成,分別為: ### 1. 最大內容繪製(Largest Contentful Paint, LCP) - 定義: 找當前viewpoint中最大的元素,衡量繪製此元素所需要的時間 => 越短越好 - 原因: 圖片或影片(banner/蓋板類型的廣告...) - 解決: 快速的fetch content檔案是首要原則 - 實作: #### HTML img tag - srcset/imagesrcset/image-set: 在不同display ratio 給不同圖片 - width/height: 讓瀏覽器預先定義繪製範圍 - loading: eager 及時loading出圖片 - fetchpriority: high 及時loading出圖片最優先級別 - link[rel="preload"] 讓瀏覽器預先下載圖片 ``` html= <!-- HTML Img tag --> <img src="img/img-src-1x.jpg" srcset="img/img-src-1x.jpg 1x, img/img-src-2x.jpg 2x, img/img-src-3x.jpg 3x" width="16" height="10" loading="eager" fetchpriority="high" /> <!-- link[rel="preload"] --> <link rel="preload" href="img/img-src-1x.jpg" as="image" imagesrcset="img/img-src-1x.jpg 1x, img/img-src-2x.jpg 2x, img/img-src-3x.jpg 3x" /> <!-- img in background --> <style> .banner { background-image: -webkit-image-set( url("img/img-src-1x.jpg") 1x, url("img/img-src-2x.jpg") 2x, url("img/img-src-3x.jpg") 3x ); } </style> ``` ### 2. 首次輸入延遲(First Input Delay, FID) - 定義: 從網頁開始render到使用者真正和網頁互動所需要的時間 => 越短越好 - 原因: 垃圾PM在網頁中塞無用的內容和功能 - 解決: 有效減少js initial 以及 excuation的時間 > 只要css可以完成的事情絕不用js - 實作: #### [carousel](https:///codepen.io/phoebe4561/pen/qBMyyzb?editors=1100) - overflow-x : scroll ⇒ 滑動效果 - 容器設定scroll-snap-type,子元素設定scroll-snap-align - scroll-snap-type:x 提升拖曳使用者體驗 不滿50%回前一頁 超過50%直接switch下一頁 - scroll-snap-align:center 捕獲滾動的對齊位置的,表示捕獲點是中間位置。 ```css= .carousel { display: flex; overflow-x: scroll; scroll-behavior: smooth; scroll-snap-type: x mandatory; } .carousel__item { width: 100%; flex-shrink: 0; scroll-snap-align: center; } ``` [How do Nike and Apple make such smooth and touch friendly carousels with pure CSS?](https://twitter.com/Steve8708/status/1531388545960583168) #### navigation - hover 是 desktop 上常見的滑鼠互動效果,但在 mobile 就沒作用了 QQ - @media 的 hover :hover 只在 hover 支援的裝置顯示 hover ``` css= // 以往可能會個別在某區設定 @media(min-width: 996px){ .box:hover{ // ... } } @media(hover :hover){ // hover 支援情況多用於 desktop 上 } @media(hover :none){ // hover 不支援多用於 pad 與 phone 上 } ``` - :focus-within ([類似Javascript 的事件冒泡](https://codepen.io/Chokcoco/pen/gjeoPg?editors=1100)) 表示當前元素或當前元素的子元素處於focus狀態時候可以做的事 ``` css= .nav-unit__submenu{ position: absolute; display: none; ... } /* make it clickable */ .nav-unit:focus-within .nav-unit__submenu{ display: none; } /* desktop user */ @media(hover:hover) and (min-width:768px){ .nav-unit:hover .nav-unit__submenu{ display: block; } } ``` #### [rolling messages](https://codepen.io/phoebe4561/pen/GRXXQmz?editors=0100) - 最新消息布達 - animation 搭配 animation-play-state: paused 暫停動畫 ``` css= .marquee{ animation: marquee 5s linear infinite; } @keyframes marquee { from { transform: translateY(0); } to { transform: translateY(-75%); } } @media(hover:hover){ .marquee:hover { animation-play-state: paused; } } ``` #### mutation (container query 容器查詢) - RWD => CSS Media Query - 限制:同瀏覽器寬度,同組件所在的容器寬度不一樣,難以統一進行rwd設計 - 目標:希望元件可以直接根據父元素的寬度來反應變化,而不是瀏覽器的 viewport大小。 ![](https://i.imgur.com/vN3zXj0.png) create a base component, and then make variations of it ``` css= .c-article { /* The default, stacked version */ } .c-article > * + * { margin-top: 1rem; } /* The horizontal version */ @media (min-width: 46rem) { .c-article--horizontal { display: flex; flex-wrap: wrap; } .c-article > * + * { margin-top: 0; } .c-article__thumb { margin-right: 1rem; } } ``` - 常見問題: - [情境: Consider that we want to use the default .c-article in the main section=>畫面變形] ![](https://i.imgur.com/qp1vbo4.png) - 解決方式:使用container query 容器查詢 - The purple outline represents the parent width. Notice how when it gets bigger, the component adapts to that. ![](https://i.imgur.com/Qfjh5d4.png) ``` html= <div class="o-grid"> <div class="o-grid__item"> <article class="c-article"> <!-- content --> </article> </div> <div class="o-grid__item"> <article class="c-article"> <!-- content --> </article> </div> </div> ``` ``` css= .o-grid__item { container-type: inline-size; /*等同於contain: layout inline-size;*/ } .c-article { /* The default style */ } @container (min-width: 400px) { .c-article { /* The styles that will make the article horizontal** ** instead of a card style.. */ } } ``` - 實作 1. add the container-type property (Since a component will adapt based on its parent width, we need to tell the browser to only repaint the affected area, not the whole page) > container: <container-name>/ <container-type>; - container-type: The type of containment context 表示指向容器的類型 ``` css= container-type: normal; /*default,不建立容器元素*/ container-type: size; /*水平和垂直方向都建立*/ container-type: inline-size; /*只在水平方向建立,會給元素同時應用layout和inline-size容器狀態 (means to respond to the parent’s width changes only)*/ ``` - container-name(optional): A case-sensitive name for the containment context ``` css= .container-a { container: inline-size aside; } .container-b { container: inline-size banner; } @container banner ( max-width : 480px ) { p { font-weight : bold; } } ``` 2. We defined the element .o-grid__item as a containment parent for the .c-article within it. - CSS Containment - 通過允許開發者將某些子樹從頁面中獨立出來,從而提高頁面的性能。如果瀏覽器知道頁面中的某部分是獨立的,就能夠優化渲染(也就是只重新渲染該容器)並獲得性能提升。 - css-contain => 告訴瀏覽器,指定特定節點是獨立的 => 控制頁面的重繪(Repaint)與回流(Reflow) ![](https://i.imgur.com/vmk2dgi.png) - 語法: ``` css= div { contain: none; /* 表示元素正常渲染,沒有包含規則 */ contain: layout; /* 表示元素外部無法影響元素内部的布局,反之亦然 */ contain: paint; /* 表示這個元素的子孫節點不會在它邊緣外顯示。 */ contain: size; /* 表示這個元素的尺寸计算不依賴於它的子孫元素的尺寸 */ contain: content; /* = contain: layout paint */ contain: strict; /* = contain: size layout paint */ } ``` > contain: layout > 只會影響到該節點內部的佈局,所有內部的改變都不會影響外部頁面的佈局,表示元素外部無法影響元素內部的佈局。 > 如果設置了 layout 屬性的元素被遮擋,則瀏覽器會把該元素的處理放到較低的優先級。 3. The next step is to add the styles we want to make container queries work. #### direction and writing mode (css logical porperies) - css盒模型 => 物理屬性 => 受制於書寫模式 => ex: 跨國網站 注入不同的css for不同的writing mode ![](https://i.imgur.com/tozu6eI.png) ![](https://i.imgur.com/WYo8osM.png) ``` html= // 代表icon <svg class="icon">....</svg> <span class="text">CSS is really good</span> ``` ``` css= .icon { margin-right: 0.5em; } ``` ![](https://i.imgur.com/5Ivatp4.png) RTL ``` css= html[lang="ar"] .icon { /* 表示當前文本是阿拉伯語 */ margin-right: 0; margin-left: 0.5em; } ``` - 解決 - [css logical porperies](https://codepen.io/aardrian/pen/bGGxrvM?editors=1010):邏輯屬性中沒有方向性的概念,不受書寫模式的干擾。(Skier's left: 指滑雪運動員面向山下時,他左面的那一側) ``` css= .icon { margin-inline: 0.5em; } ``` - 定義新的參考點,塊軸(block)與行內軸(inline)=> 方向不固定 : 透過HTML的dir屬性/CSS的direction等writing-mode屬性修改 ![](https://i.imgur.com/Bubyl0x.png) ![](https://i.imgur.com/OxCdSBS.png) - 傳統的定位屬性是top,right、bottom和left => 採用start和end => `block-{start|end}` `inline-{start|end}` ``` css= .element { position: absolute; block-start: 10px; inline-start: 20px; } .element { position: absolute; top: 10px; left: 20px; } ``` - 尺寸: `width = inline-size` `height = block-size` - 內外距離: `{padding|margin}-{block|inline}-{start-end}` ![](https://i.imgur.com/HMCN1hw.png) #### [HTML Anchor](https://tw.bid.yahoo.com/category/overview?guccounter=1#cateId-2092101502) - 分享後的網址 : 網址點開後要展開某個元件 + scroll到那個元件 - :target ``` css= .category-unit { position: relative; } .category__anchor { position: relative; inset-inline-start: 0; inset-block-end: -100px; } .category__unit__detail { display: none; } /* 當#cateId被target後 他下面的元素detail就會被展開 */ #cateId-1:target ~ .category__unit__detail { display: block; } ``` #### [HTML Dialog](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#examples) - 讓Dialog永遠在最上層顯示 - 使用原生<Dialog> ``` css= /* overlay */ dialog::backdrop{ background: #232a31; filter:opacity(.6); backdrop-filter: blur(3px); } /* animation for open and close */ dialog[open], dialog[open]::backdrop{ animation: dialog-open 400ms ; } ``` > 建議瀏覽器原生有支援的就使用原生 > 1. 更加語意化 > 2. 執行效率更好 #### [HTML Detail](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) - 內容展開收合 ``` html= <details> <summary>details</summary> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Error obcaecati quia qui. Fugit consequatur cupiditate unde itaque, aliquam ducimus ullam. </details> ``` ### 3. 累計版面配置轉移(Cumulative Layout Shift, CLS) - 定義: 網頁正處於Loading時,網頁上的元素(render時頁面被撐開)意外造成版面漂移的現象 => 數字越小越好,甚至為0 - 原因: 盡可能讓server side做比較少事情,讓client做render,但若元件沒處理好(字體、圖像、影片、聯繫表單或是按鈕等),版面漂移 - 解決: 制約模組呈現的高度 - aspect-ratio 明確定義元件寬高比例 - contain:content 限定元件內部才會repaint ``` css= .aspect-ratio{ aspect-ratio:1/1; } .module{ contain:content; content-visibility: auto; content-instrinsic-size: auto var(--basis-contain-intrinsic-size, 300px) } ``` - content-visibility:auto 搭配 content-instrinsic-size: auto #### content-visibility > The content-visibility CSS property controls whether or not an element renders its contents at all, along with forcing a strong set of containments, allowing user agents to potentially omit large swathes of layout and rendering work until it becomes needed. Basically it enables the user agent to skip an element's rendering work (including layout and painting) until it is needed — which makes the initial page load much faster. content-visibility屬性控制一個元素是否渲染其內容 ``` css= content-visibility: visible; 預設值,沒有任何效果 content-visibility: hidden; content-visibility: auto; ``` > content-visibility: hidden => 需要被頻繁切換顯示、隱藏的元素 > 其元素的子元素將被隱藏(但隱藏內容的渲染狀態會被緩存)=>所以當它被移除或者設為可見時,瀏覽器不需要重新渲染 > content-visibility: auto => 實現虛擬列表 > 若沒在當前viewpoint內不會render > 需搭配contain-intrinsic-size: 控制由 content-visibility 指定的元素的大小(給未被實際渲染的vw之外的元素指定高度) > contain-intrinsic-size設置auto會紀錄元件最後一次render的寬和高度(累校正回歸) ![](https://i.imgur.com/SvYDcMu.png) [卷軸抖動](https://codepen.io/Chokcoco/pen/rNJvPEX?editors=1100) - content-visibility: auto !== Lazyloading - 設定了 content-visibility: auto 的元素在可視區外只是未被渲染,但是其中的靜態資源仍舊會在頁面初始化的時候被全部載入。 - 設定了 content-visibility: auto 的未被渲染的元素,不會影響全域搜尋功能 ## Devtools - performance insights => 檢視web vitals ![](https://i.imgur.com/mV5Ifg9.png) - lighthouse ![](https://i.imgur.com/mzfj7Um.png) - accessibility tree => 無障礙設計 ![](https://i.imgur.com/T4kuFGQ.png) > Web accessibility (網頁可訪性) means that people with disabilities can use the Web. - css overviews ![](https://i.imgur.com/qOoO8Ip.png) - dark mode ![](https://i.imgur.com/X2uru9L.png) ## 結論 1. LCP => 找到最大元件, 盡可能最快fetch到這個項目 2. FID => 能用native不要手刻/能用css不用js/和PM溝通 3. CLS => 及早定義元件寬高 ## 參考資料 [CSS Container Queries](https://ishadeed.com/article/say-hello-to-css-container-queries/) [CSS Containment](https://zhuanlan.zhihu.com/p/401346785) [CSS邏輯屬性1](https://web.dev/learn/css/logical-properties/) [CSS邏輯屬性2](https://adrianroselli.com/2019/11/css-logical-properties.html) [content-visibility](https://tw511.com/a/01/44898.html)