
上一份工作交接的時候我接手了一個快兩年沒怎么大改的移動端H5項目。照例先翻package.json做技術(shù)摸底看到react-fastclick還躺在 dependencies 里說實話心里咯噔了一下。這個庫在我上一個項目里早就被刪干凈了但等我再仔細翻代碼發(fā)現(xiàn)項目里保留了不少針對舊內(nèi)核WebView的兼容邏輯于是又猶豫了到底該不該繼續(xù)用 react-fastclick 來應(yīng)對移動端 300ms 點擊延遲這個問題沒有一句“早就不需要了”那么簡單。如果你也在維護老項目或者新項目里還在糾結(jié)要不要引入這個庫這篇文章把 300ms 點擊延遲的來龍去脈、FastClick 的前世今生、以及現(xiàn)代移動端瀏覽器到底做了什么改進全部攤開講清楚。你只要對照自己的項目場景就能得出明確結(jié)論不用再為這個事兒開會討論。1. 300ms點擊延遲的來龍去脈1.1 雙擊縮放是怎么把延遲“發(fā)”給全世界的2007 年 iPhone 發(fā)布的時候移動端的網(wǎng)頁交互其實處在一個非常尷尬的階段。桌面端頁面直接搬到手機上字太小用戶需要雙擊才能把某個區(qū)塊放大看清內(nèi)容。為了保證“第一次點擊”和“第二次點擊”能被區(qū)分開——也就是系統(tǒng)需要判斷你到底是想雙擊縮放還是單純地單擊某個按鈕——Safari 在瀏覽器內(nèi)核里加了一個等待機制第一次觸摸事件發(fā)生之后不立刻派發(fā) click而是等 300ms 左右確認你沒有第二次點擊才把 click 事件發(fā)出來。這個 300ms 的等待邏輯本質(zhì)上是給用戶一個手勢選擇的“猶豫期”。后來 Android 上的 Chrome 和各個第三方瀏覽器幾乎都復(fù)刻了同樣的行為因為雙擊縮放在當(dāng)時是移動端瀏覽網(wǎng)頁的基礎(chǔ)交互沒有它用戶根本沒法閱讀那些沒為手機優(yōu)化過的頁面。等移動端H5開發(fā)成為主流之后大家才意識到這個“猶豫期”在按鈕點擊場景里有多難受。關(guān)鍵點在于300ms 不是手機硬件的觸摸采樣延遲也不是 WebView 渲染的卡頓而是瀏覽器主動設(shè)置的一個事件派發(fā)等待窗口。所以它跟手機性能好壞關(guān)系不大iPhone 15 和 iPhone 6 上如果不做處理點擊事件的延遲感受是一樣的。1.2 300ms延遲真正的影響范圍有多大說起來300ms 好像也就是一眨眼的功夫但在真實交互里體感非常明顯。你點一個按鈕按鈕本身沒有反應(yīng)大約 0.3 秒之后才開始觸發(fā)視覺反饋用戶的第一感覺就是“卡”“不跟手”。尤其在做移動端性能優(yōu)化的時候整個頁面可能渲染已經(jīng)優(yōu)化到 60fps 了結(jié)果點擊事件還帶著 300ms 的鐵鏈前端的交互體驗拉胯優(yōu)化其他東西都白搭。更讓人頭疼的是衍生出來的“點擊穿透”問題。移動端彈窗的關(guān)閉按鈕、輪播圖的切換箭頭、表單里的提交按鈕這些高頻點擊區(qū)域一旦有 300ms 延遲用戶就容易產(chǎn)生二次點擊、甚至三次點擊。層疊結(jié)構(gòu)下上面圖層點擊后消失下面圖層在同位置又接收到了這個 click就出現(xiàn)了關(guān)閉彈窗的同時把底下某個按鈕也一起觸發(fā)的情況。這也是為什么當(dāng)年 FastClick 那么火的直接原因——它不只消除延遲還順手解決點擊穿透。從數(shù)值上算一個用戶一天在移動端H5上點擊按鈕、鏈接、Tab 至少幾十次每次多等 0.3 秒累積起來就是十幾秒的白白等待。這些等待體感不會直接報錯但跳出率會誠實地告訴你問題有多嚴重。2. 現(xiàn)代瀏覽器早就把這個延遲“干掉了”2.1 viewport設(shè)置對了一半問題就沒了很多 React 項目里的public/index.html都寫著一行meta nameviewport contentwidthdevice-width, initial-scale1.0但這行代碼在消滅 300ms 點擊延遲這件事上作用被嚴重低估了。Google 的 Chrome 團隊在 2013 年就提出了一個方案當(dāng)頁面設(shè)置了widthdevice-width的 viewport意味著頁面寬度與設(shè)備寬度一致用戶不需要雙擊縮放來閱讀內(nèi)容那瀏覽器可以直接關(guān)閉雙擊縮放檢測自然也就不用等待 300ms。這個改動真正落地是在 2014 年左右發(fā)布的 Chrome 32 上Android 端的 Chrome 從此開始對“沒有啟用縮放”的頁面取消 300ms 延遲。iOS 的 Safari 走得稍微慢一點從 iOS 9.32016年開始才跟隨這個邏輯。只要 viewport 里的width等于device-widthSafari 就不會再等待雙擊手勢。到 2017 年主流的 iOS 和 Android 瀏覽器已經(jīng)全部完成了這項調(diào)整。所以你發(fā)現(xiàn)沒有如果你的 H5 頁面從一開始就設(shè)置了正確的 viewport 且沒有開放用戶縮放那 300ms 延遲在現(xiàn)代瀏覽器里根本不存在。讓你產(chǎn)生“點擊不跟手”感覺的往往是別的因素比如頁面卡頓、事件綁定延遲、或者你在舊內(nèi)核的 WebView 里測試。2.2 touch-action: manipulation一行CSS的降維打擊如果擔(dān)心 viewport 沒被正確設(shè)置、或者有些不走尋常路的瀏覽器不認這一套那就直接用 CSS 的touch-action屬性來做兜底。html, body { touch-action: manipulation; }這一行代碼告訴瀏覽器這個頁面允許用戶進行 pan 和 pinch 縮放但不要為“雙擊縮放”做等待。瀏覽器收到指令后會直接跳過雙擊手勢檢測點擊事件立刻派發(fā)。相比引入 FastClick這幾乎是零成本、零維護的降維打擊。touch-action的兼容性也相當(dāng)不錯。iOS Safari 9.3、Chrome 36、Android WebView 36 全都支持。如果你的項目最低要兼容到五六年前的移動端瀏覽器這一行 CSS 也基本夠用。唯一的風(fēng)險是touch-action本身會改變?yōu)g覽器對手勢行為的一些判定但在manipulation這個值上它允許所有滾動和捏合縮放保留頁面所有默認能力,只是移除了雙擊縮放等待適合絕大多數(shù)頁面場景。2.3 Pointer Events新一代點擊事件協(xié)議除了 CSS 層面DOM 事件層面也有了一個統(tǒng)一方案Pointer Events。這個協(xié)議把鼠標(biāo)事件、觸摸事件、觸控筆事件統(tǒng)一成一套pointerdown、pointerup、pointermove并且由瀏覽器自己決定是否需要等待手勢判定。在實際業(yè)務(wù)里絕大多數(shù)現(xiàn)代框架React、Vue的onClick底層仍然綁定的是 click 事件所以 Pointer Events 更多是給那些需要“超低延遲”反饋的場景使用的。比如一個拖動滑塊、繪圖板、或者高頻點擊的按鈕你可以監(jiān)聽pointerdown而不是click這樣事件在手指接觸屏幕的一瞬間就會觸發(fā)而不是等觸摸結(jié)束再觸發(fā)。這在交互手感上差距非常明顯。當(dāng)然直接全面切換到pointerdown也有新問題它不具備 click 的“容錯性”用戶在頁面滑動時一開始的pointerdown也會被誤判為點擊。所以更務(wù)實的做法是常規(guī)按鈕繼續(xù)用 click touch-action: manipulation只有對延遲特別敏感的自定義組件才用pointerdown自己控制手感。3. react-fastclick的功勞與歷史包袱3.1 FastClick的工作原理react-fastclick 本質(zhì)上不是 React 組件它是 FastClick 庫的 React 封裝。FastClick 的原始思路可以用一句話概括在瀏覽器還死守著 300ms 延遲的年代不等原生的 click 事件而是自己監(jiān)聽觸摸事件在touchend之后立刻派發(fā)一個合成 click 事件。它的大致流程是這樣的在 document 上監(jiān)聽touchstart和touchend記錄觸摸開始和結(jié)束的位置與時間。如果觸摸位移很小基本沒有滑動并且時長很短比如小于某個閾值就判定這次觸摸是“點擊”立即在目標(biāo)元素上觸發(fā)一個合成 click。與此同時它內(nèi)部還會阻止瀏覽器隨后派發(fā)的原生 click避免同一個點擊被執(zhí)行兩次。在當(dāng)時那個瀏覽器環(huán)境碎片化嚴重的年代FastClick 確實解決了很多實際問題。不管是在 iOS 還是 Android 的舊瀏覽器里引入它之后點擊響應(yīng)立刻跟手用戶體感提升非常明顯。一個庫能被幾乎全行業(yè)廣泛采用并且一度成為移動端H5項目的標(biāo)配說明它的價值在當(dāng)時是貨真價實的。3.2 它解決問題同時也在制造新問題FastClick 被詬病最多的問題集中發(fā)生在表單交互上。iOS 下點擊 input 輸入框偶爾會出現(xiàn)“無法聚焦、需要點多次才彈出鍵盤”的怪異現(xiàn)象社區(qū)里大量 issue 都指向 FastClick 與輸入框聚焦事件狀態(tài)機之間的沖突。很多人不得不額外加一段 hack 代碼來修復(fù)。第二個副作用是它會影響瀏覽器原生的一些手勢判定。如果你在頁面上實現(xiàn)了一個自定義滾動容器或者拖拽組件FastClick 的 touch 監(jiān)聽有可能把手勢期間的一次“輕微位移”誤判成“快速點擊”結(jié)果拖拽剛啟動就觸發(fā)了點擊回調(diào)。這些場景排查起來非常耗時間因為它不是必現(xiàn)的跟用戶的觸摸軌跡、手指濕度都有關(guān)系。第三個問題是技術(shù)棧層面的。React 17 把事件委托的掛載點從 document 改成了應(yīng)用的根容器FastClick 攔截 document 級 click 的機制在某些情況下會和 React 的事件系統(tǒng)產(chǎn)生沖突。前端框架迭代到 React 18、19 之后react-fastclick 這個庫已經(jīng)基本停更代碼倉庫里的 issue 長時間沒人回復(fù)。你在 npm install 它的時候可能還會看到一堆因為依賴過老而出現(xiàn)的警告。3.3 react-fastclick的現(xiàn)狀與維護情況我特意去翻了一下 react-fastclick 和 FastClick 的 npm 及 GitHub 倉庫。FastClick 本體最后一次正式發(fā)布停留在 2016 年之后react-fastclick 的更新節(jié)奏同樣緩慢最近幾年的變化主要是為了適配 React 的版本兼容補丁還是靠社區(qū)貢獻者在維護。這不代表它已經(jīng)完全不可用你的項目只要鎖住了 React 版本它也許還能跑得不錯但問題在于它的設(shè)計初衷是為了解決“老瀏覽器普遍有 300ms 延遲”的年代問題而這個前提在今天已經(jīng)不存在了。拿一個簡單的例子來說今天你新開一個 React 18 的移動端H5項目如果只面向現(xiàn)代手機瀏覽器占比超過 95%引入 react-fastclick 等于給代碼庫增加了一個不必要的第三方依賴還額外承擔(dān)了它和 React 事件系統(tǒng)“磨合”的風(fēng)險。你維護的每行依賴都應(yīng)該有它存在的理由而不是因為“以前一直這么用”就一直留著。4. 你的項目到底該不該繼續(xù)用4.1 先做一次項目環(huán)境“體檢”要回答“該不該用”需要先搞清楚你的項目跑在什么環(huán)境下。我建議從下面幾個角度做一次快速體檢頁面 HTML 里有沒有設(shè)置meta nameviewport contentwidthdevice-width, initial-scale1.0viewport 如果缺失絕大多數(shù)現(xiàn)代瀏覽器依然可能保留雙擊縮放檢測。你的用戶是用系統(tǒng)瀏覽器打開還是內(nèi)嵌在某個 App 的 WebView 里微信內(nèi)置瀏覽器、企業(yè) App 的 WebView它們的瀏覽器內(nèi)核更新時間一般滯后于系統(tǒng)瀏覽器。你實際需要兼容的最低內(nèi)核版本是什么如果項目后臺統(tǒng)計里還有大量 Android 7 以下的老設(shè)備訪問或者公司內(nèi)部 App 的 WebView 是基于多年前的 X5 內(nèi)核那情況要另說。頁面上有沒有大面積的文本縮放需求如果你的產(chǎn)品允許用戶縮放頁面來讀書那就不能直接關(guān)閉瀏覽器縮放能力需要更精細地管理手勢和事件派發(fā)。一個很粗暴的判斷方法是把 react-fastclick 從代碼里摘掉加上touch-action: manipulation用真機跑一輪核心操作流程看點擊反應(yīng)是否滿足產(chǎn)品預(yù)期。如果滿足就不需要再引入如果不滿足再考慮針對性方案。4.2 不同場景下的推薦方案場景推薦方案現(xiàn)代H5項目viewport正確目標(biāo)用戶設(shè)備較新不引庫加一行touch-action: manipulation即可老App內(nèi)嵌WebView內(nèi)核版本接近現(xiàn)代系統(tǒng)瀏覽器不引庫先用 CSS 兜底真機測試后再做決定老App內(nèi)嵌WebView基于多年前的定制內(nèi)核可以暫時保留 react-fastclick同步打算治理兼容層頁面需要支持復(fù)雜的滑動和縮放交互不使用 FastClick改用 Pointer Events 自己控制手勢判定React 組件庫自帶觸摸反饋盡量使用組件庫的觸摸事件不額外疊全局庫4.3 如果決定移除怎么安全下線如果你的項目里還在用 react-fastclick經(jīng)過體檢后決定移除我建議按下面步驟操作每一步都做回歸驗證不要在凌晨發(fā)布前臨時刪依賴。第一步全局搜索代碼里所有FastClick或react-fastclick的引用理清楚它是在入口文件統(tǒng)一初始化還是在多個頁面里分散調(diào)用。很多老項目只在入口文件調(diào)用了一次FastClick.attach(document.body)搜索起來很快。第二步移除依賴并刪除初始化代碼同時在全局樣式里加上touch-action: manipulation。這里要強調(diào)可以先加 CSS、不刪 JS發(fā)一版到測試環(huán)境觀察沒問題再徹底刪這種灰度策略雖然多了一次發(fā)布但能把風(fēng)險拆分得很干凈。第三步重點回歸表單交互。盡可能在 iOS 和 Android 的真機上測一遍輸入框聚焦、鍵盤彈起、下拉選擇這些操作確保沒有出現(xiàn) FastClick 曾經(jīng)帶來的“點一次沒反應(yīng)”的問題。第四步手動觸發(fā)一遍“快速連點”場景。比如彈窗里的“確認”按鈕用戶手滑雙擊不要讓同一個彈窗回調(diào)執(zhí)行兩次。如果產(chǎn)品上確實有防止誤觸的需求直接加一個按鈕級別的 loading 狀態(tài)或者節(jié)流處理比依賴事件庫更可靠。5. 移動端點擊相關(guān)的兩個高頻衍生問題5.1 ECharts在移動端“點不動”“滑不動”是怎么回事很多團隊把 ECharts 圖表嵌到移動端H5里上了線上之后就收到反饋“我點圖上的柱狀圖沒反應(yīng)”“tooltip 不出來”“圖表頁面左右滑動特別卡”。這里要注意ECharts 在移動端的點擊問題和 300ms 點擊延遲其實是兩回事。ECharts 默認使用 Canvas 渲染圖表本身是畫在 canvas 上的一個“整體”。整個圖表區(qū)域在 DOM 上只有一個 canvas 節(jié)點ECharts 內(nèi)部會自己計算你點的是哪根柱子、哪個坐標(biāo)、哪個數(shù)據(jù)項。如果你的頁面或者父級容器設(shè)置了某些 touch-action 攔截或者 canvas 的觸摸事件沒有正確綁定到 ECharts 的 zrender 實例上就會出現(xiàn)點不到數(shù)據(jù)項的情況。比較實用的排查思路是先在 PC 端用開發(fā)者工具模擬移動端確認圖表交互本身是好的。然后到真機上打開 vConsole在頁面上插入一個調(diào)試面板監(jiān)看一下 touch 事件到底有沒有觸發(fā)是被哪一層元素攔截了。ECharts 的坐標(biāo)系轉(zhuǎn)換在移動端有時候會有一點偏移如果你用了tooltip的 HTML 模式還需要給 tooltip 特殊設(shè)置 position否則它可能被 canvas 區(qū)域的 overflow 裁剪掉。對于“圖表區(qū)域滑不動頁面”的問題可以給圖表容器設(shè)置touch-action: pan-y讓垂直方向的滾動交給頁面處理水平方向留給圖表操作。5.2 點擊穿透問題在頁面里的排查技巧點擊穿透是 300ms 延遲時代最經(jīng)典的衍生 bug。雖然現(xiàn)代瀏覽器已經(jīng)沒有延遲但如果你還在老內(nèi)核 WebView 里或者頁面上有透明遮罩、定位浮層依然可能踩到。我自己排查這類問題時第一件事是打開 vConsole在移動端瀏覽器里注入一個調(diào)試面板方便直接看控制臺日志確認到底哪個元素最終接收了 click 事件。排查思路分三步打開 vConsole 后觀察點擊浮層和底層元素時控制臺輸出的 touchstart、touchend、click 順序然后檢查浮層元素的pointer-events屬性確認它是否在某些狀態(tài)下變成了 none導(dǎo)致事件穿透到了底層最后看底部元素的點擊事件綁定是否用了事件委托如果是縮小委托范圍或改用數(shù)據(jù)屬性判斷目標(biāo)來源。常用的修復(fù)方案有三種。第一在浮層關(guān)閉后的同一幀內(nèi)給底層元素臨時設(shè)置pointer-events: none再在下次點擊時恢復(fù)這是最直接的方式。第二把浮層關(guān)閉的動畫和事件派發(fā)錯開等動畫完全結(jié)束后再允許點擊。第三如果整個頁面都在統(tǒng)一的觸摸事件模式控制下可以放棄原生 click統(tǒng)一用touchend配合位移判斷來實現(xiàn)按鈕點擊但這套方案成本更高不建議在常規(guī)頁面上引入。6. 常見問題速查表問題結(jié)論新移動端H5項目還要引入 react-fastclick 嗎絕大多數(shù)不需要除非你的目標(biāo)環(huán)境是老內(nèi)核 WebViewFastClick 還兼容 React 18/19 嗎庫基本停更官方?jīng)]有明確適配社區(qū)有補丁但建議慎用一行 CSS 能替代整個庫嗎t(yī)ouch-action: manipulation在現(xiàn)代瀏覽器上可以舊內(nèi)核需要測試移除 react-fastclick 后點擊還是慢檢查 viewport、touch-action、頁面渲染性能不要盲目重裝庫ECharts 移動端點擊不生效多半不是 300ms 延遲優(yōu)先排查 canvas 和 touch-action 沖突頁面上輸入框聚焦不穩(wěn)定如果裝了 FastClick先考慮移除它觀察效果部分頁面想兼容極老瀏覽器怎么辦可以做按需降級只在檢測到老內(nèi)核時動態(tài)引庫這里我想額外提一句在排查移動端點擊問題時一定要先確認問題規(guī)模。如果只是某個頁面上的某個按鈕點擊慢大概率是頁面渲染卡頓或者事件綁定位置不對如果是整個應(yīng)用所有按鈕都有明顯延遲再往 300ms 延遲和瀏覽器內(nèi)核方向考慮。不要一上來就引入一個全局庫引入了之后再排查問題反而會多一個變量。另外很多團隊在遷移到 React 18 之后發(fā)現(xiàn) react-fastclick 的綁定行為有點“怪”順手把它換成了 CSS 方案結(jié)果真機上毫秒級的點擊響應(yīng)反而比之前還好。這也在預(yù)料之中因為現(xiàn)代瀏覽器對 click 的派發(fā)已經(jīng)不再有長等待把原生事件交給瀏覽器去處理永遠比第三方庫模擬事件更可靠。7. 我踩過幾次坑之后的感想從最早在 jQuery 項目里用 FastClick到 React 項目里用 react-fastclick再到現(xiàn)在新項目里已經(jīng)完全不引入這個庫我自己經(jīng)歷了完整的三個階段。回過頭來看這類“問題解決庫”的生命周期其實很有參考價值它誕生于一個特定的歷史環(huán)境解決了當(dāng)時普遍存在、但今天已經(jīng)被技術(shù)棧自身進化掉的問題。我現(xiàn)在接到老項目代碼評審看到 react-fastclick 還掛在 dependencies 里時默認流程是先檢查一遍頁面的實際運行環(huán)境做一個瀏覽器內(nèi)核版本占比統(tǒng)計然后在測試環(huán)境里直接去掉這個庫、加上一行 CSS跑一輪自動化和真機回歸。如果指標(biāo)沒有下降就直接推進刪除。如果頁面確實有相當(dāng)一部分用戶還在用老內(nèi)核 WebView那我會把 react-fastclick 限定在“僅老內(nèi)核環(huán)境按需加載”的降級策略里而不是讓所有用戶都背這份包袱。最后再分享一個實用小技巧移動端點擊這件事不要只盯著事件庫。手指按下去到頁面響應(yīng)涉及硬件觸摸采樣、系統(tǒng)手勢識別、瀏覽器事件派發(fā)、DOM 事件回調(diào)、React 狀態(tài)更新與渲染多階段任何一個環(huán)節(jié)慢都會導(dǎo)致“點擊不跟手”。先打開瀏覽器的 Performance 面板錄制一段時間用戶操作看看從touchstart到click派發(fā)的時間差是多少以及點擊后 React 組件狀態(tài)更新耗時多少。數(shù)據(jù)擺出來誰慢就優(yōu)化誰比直接引一個第三方庫靠譜得多。