一招減少 JavaScript 代碼量
如今依賴 JavaScript 提供交互的網站越來越多。雖然 JavaScript 可以提供愉快的體驗,但同時也帶來了一些負面影響:
- 頁面加載時間變長;
- 只有在 JavaScript 加載完成且正確無誤后,頁面才能使用;
- 團隊需要足夠的手段和資源來關注可用性、反應性和可訪問性。
鑒于這些缺點,我們可以依靠瀏覽器提供的原生解決方案,這種方式不僅可以降低成本,而且還可以享受社區創建的 Web 標準專業知識帶來的優勢。通常,這些解決方案的代碼量較少,因此還可以減少開發團隊的維護工作(比如無需更新使用的庫)。
在本文中,我們來探討一下部分可供大多數用戶使用的原生解決方案。我會給出一些示例,但不會深入探討所有細節。
渲染被 JavaScript 阻塞
在介紹各種技術之前,首先我需要提醒一下使用 JavaScript 的一大缺點:瀏覽器只有一個線程來控制頁面的渲染。在運行 JavaScript 時,瀏覽器會延遲用戶交互事件與界面更新。這就很討厭了,因為你會覺得頁面沒有響應你的操作,或者感覺動畫很卡。
谷歌工程師 Philip Walton 對這方面優化進行了詳細闡述,感興趣可以前往查看:https://calendar.perfplanet.com/2020/html-and-css-techniques-to-reduce-your-javascript/
開發團隊的日常工作比較喜歡功能強大的設備,因此會掩蓋 JavaScript 帶來的負面影響。但不要忘記在功能有限的設備上做定期測試。
限制顯示的行數
JavaScript版
以下是用JavaScript實現此操作的兩種方法:
- 限制顯示的字符數。這種做法很容易出錯,因為除了等寬字體之外,一般字體的寬度都是可變的。因此,最終顯示出來的文本長度往往會超過預期,或被中途截斷。
- 反復試驗元素顯示的內容,直到達到理想的行數。這種做法的代價很高,因為每次嘗試都需要瀏覽器完成渲染,才能看清顯示的內容。而且這種技術只有在使用指定字體渲染后才能得到準確的結果,這可能會導致較大的布局偏移。
當頁面包含大量需要截斷的文本時,頁面顯示會被延遲。此外,這兩種解決方案會完全截斷文本,有可能會影響到搜索引擎或輔助技術的恢復。頁面中元素的字體大小或寬度也有可能發生變化。考慮所有的情況很麻煩。
原生版
-webkit-line-clamp 是一個原生的 CSS 屬性。十年前在 Safari 中引入,如今已被廣泛使用,其他瀏覽器出于兼容性原因也采用了這個屬性,而且已成了標準。你需要一些其他帶前綴的屬性來實現所需的行為。雖然使用帶有前綴的屬性性有點煩人,但是標準中已經詳細描述了該前綴,因此這樣做不會有風險。
除了 IE 和 Firefox 68 之前的版本外,所有瀏覽器都支持該屬性。下面的例子說明了具體的使用方式。
HTML :
- <div class="demo-grid">
- <div class="demo-card">
- <img width="200" height="200" src="https://placekitten.com/200/200?image=1" alt="" class="demo-img">
- <h1 class="line-clamp demo-title">
- Spill litter box, scratch at owner, destroy all furniture, especially couch
- </h1>
- </div>
- <div class="demo-card">
- <img width="200" height="200" src="https://placekitten.com/200/200?image=2" alt="" class="demo-img">
- <h1 class="line-clamp demo-title">
- Claws in the eye of the beholder
- </h1>
- </div>
- <div class="demo-card">
- <img width="200" height="200" src="https://placekitten.com/200/200?image=3" alt="" class="demo-img">
- <h1 class="line-clamp demo-title">
- Relentlessly pursues moth eat too much then proceed to regurgitate all over living room carpet while humans eat dinner
- </h1>
- </div>
- </div>
- <!-- Titles via http://www.catipsum.com/ -->
CSS:
- /* By default, truncate the text abruptly */
- .line-clamp {
- /* Careful computing the max-height, it needs to match n * line-height */
- max-height: calc(2 * 1.15 * 1.5rem);
- overflow: hidden;
- }
- /* For capable browsers, truncate with an ellipsis */
- /* To simulate a browser without support, you can add a s after clamp in the following line */
- @supports (-webkit-line-clamp: 2) {
- .line-clamp {
- /* Remove the made up max-height */
- max-height: none;
- /* Those three properties are mandatory, so is overflow: hidden that we defined earlier */
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- }
- }
- /* Extra code for the look of the demo */
- .demo-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(13rem,1fr));
- gap: 1rem;
- margin: 1rem;
- }
- .demo-card {
- background-color: #E2E8F0;
- border-radius: 0.5rem;
- display: flex;
- flex-direction: column;
- padding: 0.5rem;
- }
- .demo-img {
- align-self: center;
- margin-bottom: 0.5rem;
- border-radius: 0.5rem;
- }
- .demo-title {
- font-size: 1.5rem;
- margin: 0;
- }
該解決方案沒有性能或頁面內容偏移的問題,而且還不會影響搜索引擎或輔助技術。但是,它不適合擁有多個子項的元素。
重要的元素始終顯示在頁面上
有時候,你希望頁面的某個部分始終顯示在視野范圍內,比如標題、工具欄或購物車。我經常遇到這種行為,但能夠正確實現的卻很少。
JavaScript 版
如果想通過 JavaScript 實現這個功能,則必須監聽頻繁觸發的滾動事件。大多數解決方案經常會通過限流或去抖動的技術去除大部分事件。現如今,我們可以使用 IntersectionObserver ,僅在元素進入或離開窗口時接收事件。這樣做的效率更高。
在檢測到元素進入或離開窗口時,我們需要從 position:relative 切換到 position:fixed 。這需要瀏覽器重新計算大量元素的大小和位置(我們稱之為頁面重新布局),這種做法的代價很大。我們需要確保周圍元素不會四處移動,而且不會導致內容跳躍。
如果在元素進入或退出窗口時,渲染被阻塞(如果在滾動的同時協調使用動畫,則很可能發生阻塞),那么切換將被進一步延遲。
原生版
CSS 有一個屬性 position:sticky 可以實現這種行為,而且還沒有性能、響應性或內容跳躍的問題:只要瀏覽器可以滾動,它就會將元素準確地定位到你聲明的位置上。你可以利用 top 、 bottom 、 left 或 right 選擇定位。
html
CSS
除了IE和舊版的 Chrome 或 Firefox 之外,所有瀏覽器都支持sticky。對于這些舊版的瀏覽器,元素僅支持默認值 position:static,而且不會處理 top、bottom、left 和 right 的值。如果你需要支持這些瀏覽器,則請記住這一點。舊版的 Safari 需要 -webkit-sticky 前綴。
但是,這個屬性有一個限制:無法根據元素是否sticky來改變元素的外觀,比如使用:stuck之類的偽類。這是 CSS常見的限制。在這種情況下,我建議使用 position:sticky 來設置sticky屬性,同時結合 IntersectionObserver來改變其外觀(注意不要改變大小,以防止內容跳躍)。
平滑滾動
JavaScript 版
如果想在JavaScript中實現這一點,你需要定期執行改變滾動位置的JavaScript。為了動畫能夠流暢地運行,在整個動畫運行過程中,不能有其他JavaScript阻塞渲染。
此外,你還需要選擇一個計時函數。為了看起來很自然,可能需要針對每個操作系統使用不同的計時函數,才能符合該操作系統的常規做法。
原生版
CSS有一個屬性 scroll-behavior: smooth 和 {behavior: 'smooth'},可以代替JavaScript的 scroll 、 scrollTo 和 scrollIntoView ,將所有有關計時的決定都交給CSS。這樣可能更符合常用設備的常規做法。
Safari 尚不支持此功能(除非啟用隱藏選項),但大多數情況下,這不是什么大問題。
html
CSS
無論是JavaScript版還是原生版,你都需要注意兩個可訪問性方面的問題:遵循盡可能減少動畫和頁面移動的設置,以及確保焦點正確移動。
滾動到吸附點
通過這種方式,可以創建幻燈片、水平列表,吸附到每張圖片或每一節,讓它們占據整個窗口。
JavaScript版
為了創建幻燈片,我們需要監聽:
- 鼠標點擊事件( mousedown 、 mouseup 、 touchstart 、 touchend 、 pointerdown 或 pointerup );
- 移動事件( mousemove 、 touchmove 或 pointermove )。
正確處理所有指針事件(鼠標事件或觸碰事件),并處理鼠標指針離開區域的事件非常需要技巧。如果能正確處理這些事件,就可以相應地移動元素。每次移動都可能導致昂貴的重新布局,破壞顯示效果。
如果每一節都占據整個窗口或遇到水平列表,我們必須監聽所有滾動事件,并用我們需要的滾動處理替換。獲得理想的效果非常困難,因為我們需要完整地控制原生的滾動行為。
不論何種情況,你都需要根據原始的頁面移動速度和距離來確定是否應該移動到下一個項目。如果你的選擇不符合系統的行為,就會給用戶造成困擾。
原生版
CSS的滾動吸附功能可以處理該行為。在滾動的容器中,定義 scroll-snap-type 來指示吸附的方向,以及吸附必定發生還是僅在接近吸附點時發生。然后在容器的子元素中定義 scroll-snap-align 來標明吸附點。
下面的演示完全沒有使用JavaScript。它還使用了 scroll-behavior 來提示用戶使用正常的滾動機制。
選中復選框,即可使用 IntersectionObserver 在縮略圖中高亮顯示當前的圖片。
html
CSS
JS
所有現代瀏覽器都支持該行為。還有另一種語法(https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scroll_Snap/Browser_compat)但我不建議使用。它只會增加測試的負擔,而你可以依靠優雅降級來解決該問題。在不支持滾動吸附的瀏覽器中,該功能將會降級為正常的滾動。
不論對于鼠標的情況還是觸摸屏的情況,由于使用了瀏覽器的滾動功能,該方法要比JavaScript的方法流暢許多。
延遲圖像加載
JavaScript版
用JavaScript實現該功能需要使用類似于 <img src="..." srcset="..." alt="..."> 的語法。當圖像接近視口時,使用Javascript改變圖像的屬性,以加載并顯示圖片。
這種方法的主要缺點就是,在相應的JavaScript執行之前圖像不會被顯示。而且這種情況發生的頻率遠超你的想象。搜索引擎也很難看到圖像,因為本質上圖像并不存在,而且爬蟲也不會滾動屏幕。
選擇何時觸發加載非常重要。怎樣根據當前的帶寬來決定當圖像距離視口多遠時進行加載?是否應當考慮滾動的速度?
原生版
去年,所有瀏覽器(Safari除外)都實現了 <img> 元素的 loading="lazy" 屬性。如果你的網站會加載所有圖像,那么可以嘗試下這個屬性。幾乎不需任何代價,就可以讓網站加載變快。
如果你已經使用了某種延遲加載技術,那么在Safari支持該屬性之前,你需要根據自己的情況做決定。是否值得放棄Safari的延遲加載,來換取更簡單的代碼?
目前,觸發下載的規則由各個瀏覽器決定,可能不是最佳時機。不過有一點可以確定,瀏覽器的決定會越來越理想,而不需要改變任何代碼!
總結
我希望這篇文章可以給你一些啟示,下次在尋找某個 JavaScript 庫來實現某項功能時,可以考慮一下這些技術。此外,你也可以看看其他我沒有提及的 HTML 或 CSS 技術(比如 <details> 和 <summary> ,或 <datalist> )。瀏覽器在不斷發展,會不斷帶來驚喜,用戶也會受益!