化實戰(zhàn):用ponytail自動生成CSS雪碧圖)
前端優(yōu)化做到最后往往卡在一個很不起眼的小環(huán)節(jié)上那幾十個幾KB的小圖標(biāo)到底要不要合并成雪碧圖。我最早做這事是手動在Photoshop里拼圖、量坐標(biāo)后來接觸了ponytail這個構(gòu)建插件才發(fā)現(xiàn)雪碧圖完全可以交給構(gòu)建工具自動處理。這個插件的思路很干脆開發(fā)者繼續(xù)正常在CSS里寫background-imageponytail在構(gòu)建階段把符合條件的圖片自動合并成一張雪碧圖順手把CSS里的背景坐標(biāo)改好整個過程不用手工維護坐標(biāo)表。這篇文章就圍繞ponytail的定位、安裝、配置、原理和避坑經(jīng)驗展開。如果你是前端開發(fā)、構(gòu)建負責(zé)人或者正在做頁面性能優(yōu)化但被零散小圖標(biāo)折騰得頭疼這篇內(nèi)容可以直接當(dāng)作接入指南來用。1. ponytail到底是什么一場對傳統(tǒng)雪碧圖的自動化改造很多剛接觸的人會問雪碧圖不是老古董了嗎HTTP/2都普及了還有必要搞這個這個問題很實際但先別急著下結(jié)論我們把ponytail做的事和傳統(tǒng)方案放在一起看。1.1 傳統(tǒng)雪碧圖為什么讓人又愛又恨雪碧圖的原理不復(fù)雜把多張小圖平鋪在一張大圖里通過background-position控制顯示區(qū)域減少圖片請求次數(shù)。好處肉眼可見壞處也肉眼可見——維護成本非常高。我最早在團隊里做雪碧圖流程大概是這樣的設(shè)計師導(dǎo)出圖標(biāo)我用腳本把所有PNG拼到一張畫布記下每張圖左上角的坐標(biāo)再把坐標(biāo)寫進一個_sprite.scss變量文件里。比如$icon-add-x: -20px; $icon-add-y: -40px;業(yè)務(wù)代碼引用時再拼到background-position上。這套流程聽起來有序?qū)嶋H一改就亂。新增一個圖標(biāo)得重新拼圖、重新量坐標(biāo)、重新出圖然后全局排查哪些地方用了舊圖。最難受的是拼圖工具或手動排版總會留下空隙雪碧圖體積在不知不覺中膨脹。傳統(tǒng)的另一個痛點是人和人之間的協(xié)作成本。每個前端都有自己整理雪碧圖的習(xí)慣有人按業(yè)務(wù)模塊拆有人按頁面拆有人干脆把所有圖標(biāo)全放一張圖里最后誰都不敢動那張大圖因為一動就牽一發(fā)動全身。這些問題不是雪碧圖方案本身的問題而是手工維護帶來的問題。1.2 ponytail的核心定位與設(shè)計思路ponytail做的最重要一件事是把“人為協(xié)調(diào)的工序”變成“構(gòu)建期的純代碼操作”。它本質(zhì)上是webpack構(gòu)建鏈路上的一個插件職責(zé)范圍非常聚焦掃描CSS中的圖片引用、過濾出適合合并的小圖、自動拼合成雪碧圖、重寫CSS坐標(biāo)。這個設(shè)計思路有一個核心關(guān)鍵詞約定優(yōu)于配置。開發(fā)者在業(yè)務(wù)CSS里怎么寫背景圖ponytail不去干涉它默認你寫的是正??山馕龅膗rl()。構(gòu)建時插件自己判斷哪些圖該合并、哪些圖不該合并開發(fā)者不需要在代碼里標(biāo)注這個圖標(biāo)要加入雪碧圖之類的東西。這種無侵入式設(shè)計對老項目尤其友好接入時不需要大規(guī)模改動現(xiàn)有CSS。ponytail另一個值得說的設(shè)計選擇是局部自動化而不是全量暴力合成。它默認只處理小于設(shè)定閾值的圖片大圖、長圖、高清背景圖不會被塞進雪碧圖里。這個取舍很關(guān)鍵因為雪碧圖的核心收益是減少零碎小文件的請求數(shù)如果無腦把所有圖片都合成雪碧圖體積和尺寸會失控反而拖累加載。1.3 為什么不用base64、iconfont或HTTP/2替代有對比才更容易理解一個工具的定位。這幾個方案都解決小圖片請求過多的問題但適用場景差異很大。方案優(yōu)點典型問題適合場景base64內(nèi)聯(lián)零額外請求、接入簡單CSS體積膨脹約33%無法緩存復(fù)用極少量且長期不變的小圖iconfont單色圖標(biāo)表現(xiàn)好、可用CSS控制多色/復(fù)雜圖形難做、字體文件本身也要加載圖標(biāo)庫、風(fēng)格統(tǒng)一的UI圖標(biāo)HTTP/2多路復(fù)用降低連接開銷、實現(xiàn)簡單零散小圖仍各有Header損耗老設(shè)備/內(nèi)網(wǎng)環(huán)境不穩(wěn)請求數(shù)不多、以長連接為主的業(yè)務(wù)CSS Sprite請求數(shù)大幅下降、圖片可緩存維護成本高容易坐標(biāo)錯亂多小圖標(biāo)、低頻變化的業(yè)務(wù)ponytail Sprite自動合成、自動改坐標(biāo)需要構(gòu)建環(huán)境配合有webpack構(gòu)建流程的項目我的觀點是HTTP/2并不能完全替代雪碧圖。HTTP/2雖然能多路復(fù)用但每個圖片文件依然有獨立請求和處理過程幾十個小圖標(biāo)就是幾十個請求服務(wù)端日志、緩存策略、CDN回源成本都還在。而base64和iconfont各自有硬傷一個不適合批量場景一個只適合單色場景。雪碧圖的定位依然牢固最大的歷史包袱就是維護成本ponytail恰好補上了自動化這一環(huán)。2. 安裝與基礎(chǔ)接入給構(gòu)建管線加上雪碧圖能力接入ponytail的門檻很低前提是你已經(jīng)有一個webpack構(gòu)建項目并且CSS里通過相對路徑引用本地圖片。下面按步驟完整走一遍。2.1 環(huán)境準(zhǔn)備與安裝建議在Node.js 12以上的環(huán)境使用webpack 4或webpack 5都可以。安裝時作為開發(fā)依賴npm install --save-dev ponytail如果項目用的是yarnyarn add --dev ponytail裝完后可以在package.json里確認一下版本不必追求最新版本。構(gòu)建類插件跟著大版本走比較穩(wěn)新版本發(fā)布后建議先在一個分支上試跑構(gòu)建確認無報錯再升級。2.2 webpack配置接入在webpack.config.js的plugins數(shù)組里添加PonytailPluginconst PonytailPlugin require(ponytail); module.exports { // ...其他配置 plugins: [ new PonytailPlugin({ limit: 10240, // 小于10KB的圖片參與合并 outputPath: assets/img/, // 雪碧圖輸出目錄 publicPath: /static/, // 雪碧圖在瀏覽器中的訪問前綴 spriteName: sprite-[contenthash].png, include: /\.css$/, exclude: [/node_modules/, /background-pattern/] }) ] };幾個核心配置項的作用我實際測下來是這樣的limit單位是字節(jié)這是最重要的閥門。我一般設(shè)10240也就是10KB。超過這個大小的圖說明本身就不是零碎小圖標(biāo)放進雪碧圖只會讓母圖過大。outputPath雪碧圖生成到打包目錄的位置對應(yīng)webpack輸出目錄下的assets/img/。如果你希望它直接產(chǎn)出到某個靜態(tài)目錄改成相對路徑即可。publicPath瀏覽器加載時的前綴讓CSS里生成的background-image路徑能正確指向線上資源。這個值如果配錯雪碧圖會加載不到后面排查部分會細說。include/exclude限定插件掃描哪些CSS、忽略哪些文件。通常要排除node_modules避免把第三方庫里的圖片混進來。background-pattern這類大尺寸背景素材一般也建議排除。需要提醒一點不同版本的ponytail對外暴露的配置字段名可能有細微差別。有的版本用spriteName有的版本直接用filename接入前先看一眼當(dāng)前版本的README避免照著舊文檔配置報錯。2.3 最小化示例驗證我建議第一次接入時不要直接上全量配置先搞一個最小case驗證流程通不通。準(zhǔn)備一個最簡單的頁面結(jié)構(gòu)project/ ├── src/ │ ├── index.js │ ├── style/ │ │ └── main.css │ └── img/ │ ├── icon-add.png │ └── icon-check.png └── webpack.config.jsmain.css內(nèi)容就一行.btn-add { background-image: url(../img/icon-add.png); } .icon-check { background-image: url(../img/icon-check.png); }執(zhí)行npm run build然后打開打包后的CSS文件你會看到原來的兩張圖片引用被替換成.btn-add { background-image: url(/static/assets/img/sprite-xxxx.png); background-position: 0 0; } .icon-check { background-image: url(/static/assets/img/sprite-xxxx.png); background-position: -20px 0; }同時dist/assets/img/下多了一張sprite-xxx.png??吹竭@個結(jié)果就說明管線已經(jīng)跑通接下來再擴大到真實業(yè)務(wù)。2.4 接入時的三個前提第一CSS必須是可靜態(tài)解析的。如果項目里大量通過JS動態(tài)拼background-image比如el.style.backgroundImage url( path )ponytail掃描不到這部分圖片不會進雪碧圖。這類動態(tài)引用需要業(yè)務(wù)上改造或者接受它們獨立加載。第二圖片路徑要能定位到真實文件。webpack配置里用了alias別名時CSS里寫url(~assets/img/xxx.png)這種寫法ponytail不一定能解析。最穩(wěn)的方式是在CSS里用相對路徑或者確保插件配置里設(shè)置了正確的上下文路徑。第三參與合并的圖片盡量保持原樣。不要在CSS里對同一張圖做旋轉(zhuǎn)、縮放再作為背景ponytail合并的是磁盤上的原始圖片CSS濾鏡效果不會反映到合并結(jié)果里。3. 核心機制拆解ponytail是怎么做到自動化的知其然還要知其所以然這章拆一拆ponytail的內(nèi)部工作流程。理解這些機制后面排查問題會輕松很多。3.1 圖片掃描與過濾不是所有url()都值得合并ponytail的第一步是收集圖片引用。它會在resolve階段讀取webpack處理過的CSS內(nèi)容用AST或正則的方式提取所有url()引用然后做一輪過濾過濾掉data:協(xié)議的內(nèi)聯(lián)圖片它們已經(jīng)是base64了再合并沒有意義過濾掉http://、https://開頭的遠程圖片避免把遠程資源誤下載到本地過濾掉超過limit閾值的圖片過濾掉exclude規(guī)則命中的路徑和重復(fù)出現(xiàn)的同一張圖。這一步的原則是保守。寧可少合并也不能把不該合的資源塞進來。我看過有人在生產(chǎn)環(huán)境把一張200KB的橫幅圖片誤合并進雪碧圖結(jié)果整張圖變成了寬6000多像素的巨型母圖CSS里還多出一長串坐標(biāo)加載體驗反而變差了。3.2 坐標(biāo)映射與CSS重寫原理過濾完成后插件把剩余圖片打包到同一張畫布上。算法上就是經(jīng)典的矩形裝箱問題用盡量緊湊的方式排列圖片減少雪碧圖整體尺寸。排列時有間隙很正常間隙越小越好這也是內(nèi)存和體積的博弈。排列完成最關(guān)鍵的動作來了重新寫CSS。我們把原始CSS里你對圖片的引用拆開看它包含兩部分信息——第一是圖片文件本身第二是它的顯示位置。而雪碧圖把所有圖片揉到一張圖后原圖的顯示位置就不能靠原文件確定了必須告訴瀏覽器從雪碧圖的哪個坐標(biāo)開始截取。所以插件會在原CSS規(guī)則旁邊新增一些樣式聲明。比如原始規(guī)則是.icon-add { width: 20px; height: 20px; background-image: url(../img/icon-add.png); background-repeat: no-repeat; }構(gòu)建后可能變成.icon-add { width: 20px; height: 20px; background-image: url(/static/assets/img/sprite-abc123.png); background-repeat: no-repeat; background-position: 0 0; background-size: 120px 80px; }這里background-position: 0 0代表這張圖位于雪碧圖的左上角另一個圖標(biāo)如果位于右側(cè)20像素處坐標(biāo)就是負數(shù)-20px 0。插件會把每張圖左上角的坐標(biāo)取負值填進去因為正值會讓背景圖往右下移負值才是從雪碧圖里截取對應(yīng)區(qū)域。還有一個細節(jié)容易被忽視background-size要填的是整張雪碧圖的寬高而不是單個圖標(biāo)的大小。這樣寫才能保證CSS里的背景縮放按原始比例計算。如果這里漏了Retina屏上圖片顯示會模糊。3.3 多入口、多CSS文件與分包策略現(xiàn)實項目不會只有一兩張CSSwebpack會有多個入口、多個樣式文件。ponytail處理多CSS時會做全局去重和收集同一張圖片在三個CSS里都被引用插件只會把它合并一次所有CSS引用的坐標(biāo)保持一致。這比手動維護可靠得多手動方案里同一張圖被兩個CSS引用時很容易出現(xiàn)一處改了另一處沒改的問題。如果項目里既有業(yè)務(wù)圖標(biāo)又有編輯器素材、運營位素材混合在一張雪碧圖里雖然可用但會讓母圖體積和坐標(biāo)體系變復(fù)雜。我建議用多個規(guī)則的思路來分包在配置里針對不同目錄設(shè)置不同的include規(guī)則和spriteName讓插件分別生成獨立雪碧圖。需要注意的是這樣會犧牲一部分合并率換來的是各業(yè)務(wù)線可獨立更新、互不污染。3.4 輸出與壓縮細節(jié)合并完成后插件會輸出最終雪碧圖。輸出格式通常支持PNG部分版本可能支持WebP。用PNG時要注意小圖標(biāo)帶透明通道很常見PNG-8索引色就能覆蓋大部分場景插件一般會做調(diào)色板優(yōu)化直接在構(gòu)建產(chǎn)物里把多余顏色裁掉體積會比原始多張小圖的總和還小。如果你想進一步壓體積可以在ponytail之后掛一個圖片壓縮插件或接入pngquant讓它們在產(chǎn)物落盤前再處理一次。要注意壓縮與質(zhì)量之間找平衡圖標(biāo)類素材用高頻壓縮基本沒問題但含漸變、投影的小圖壓得太狠會有色帶。輸出文件的命名建議帶上[contenthash]這個hash由內(nèi)容計算而來圖片不變文件名就不變?yōu)g覽器緩存能長期生效圖片一變hash就變不會出現(xiàn)舊緩存覆蓋不了的問題。4. 實戰(zhàn)案例一個典型業(yè)務(wù)頁面的雪碧圖接入理論講了那么多下面用一個具體場景完整走一遍。這個案例是我在某個后臺系統(tǒng)改造時做的結(jié)構(gòu)簡化過但流程是完整的。4.1 項目現(xiàn)狀與優(yōu)化目標(biāo)那是一個后臺訂單列表頁不算復(fù)雜但頁面上的狀態(tài)圖標(biāo)、操作按鈕圖標(biāo)加起來有20個分布在3個CSS文件里。圖片單個體積都不大最小1.8KB最大6.4KB全是PNG小圖標(biāo)。當(dāng)時的性能報告顯示這個頁面圖片請求數(shù)超過30個其中絕大部分是這些小圖標(biāo)。我們希望把零碎小圖請求集中在1到2個雪碧圖里同時不能改動現(xiàn)有業(yè)務(wù)邏輯和頁面結(jié)構(gòu)。很多團隊接到這種需求第一反應(yīng)是找設(shè)計師重新切圖但設(shè)計師也很無奈圖標(biāo)本來就是按需更新的。如果用ponytail這個問題從根上就不再是“設(shè)計協(xié)作”問題了而是構(gòu)建產(chǎn)物問題。4.2 改造步驟實錄第一步確認所有CSS引用方式。我翻了一遍代碼統(tǒng)一把帶alias的圖片路徑改成相對路徑。這里有個經(jīng)驗相對路徑雖然丑但在構(gòu)建工具面前最穩(wěn)不會出現(xiàn)解析歧義。第二步安裝依賴并配置。我用的配置如下把閾值設(shè)在8KB因為頁面里幾個超過8KB的圖其實是插畫素材不適合合并const PonytailPlugin require(ponytail); module.exports { // ... plugins: [ new PonytailPlugin({ limit: 8192, outputPath: assets/sprite/, publicPath: /assets/, spriteName: sprite-[contenthash].png, include: /[\\/]src[\\/].*\.css$/, exclude: [/node_modules/, /[\\/]assets[\\/]big/] }) ] };第三步處理CSS中的背景簡寫。原始CSS里有不少寫法是background: #fff url(...) no-repeat left center。ponytail重寫坐標(biāo)時容易跟簡寫里的left center沖突我在實際改造時把這些地方先拆成了完整寫法也就是background-image、background-repeat、background-position分開寫。這一步雖然不是必須的但能讓重寫結(jié)果更可預(yù)測。第四步構(gòu)建。執(zhí)行npm run build構(gòu)建日志正常通過沒有報錯。接著打開dist目錄確認三件事雪碧圖文件存在、CSS里的background-image都指向新生成的雪碧圖、頁面渲染正常。如果頁面出現(xiàn)圖標(biāo)錯亂不用慌優(yōu)先排查是不是哪個背景圖被exclude規(guī)則誤傷了或者引用到了另一個CSS里同名但不同內(nèi)容的圖標(biāo)。4.3 構(gòu)建前后對比與收益改造完成后我拉了頁面數(shù)據(jù)做對比結(jié)果如下指標(biāo)優(yōu)化前優(yōu)化后圖片請求數(shù)211圖片總體積34.6KB18.4KB合并優(yōu)化后CSS增量0約2.3KB坐標(biāo)代碼頁面加載完成時間776ms緩存為空412ms緩存為空緩存命中場景21個請求各查一次緩存1個請求查一次緩存實際體感最明顯的是多圖片并行下載時的等待時間。20個小圖雖然并行但每個都有獨立請求頭和解析過程HTTP/2下也有開銷改成一張雪碧圖后網(wǎng)絡(luò)往返次數(shù)大幅減少弱網(wǎng)環(huán)境下提升尤其明顯。而CSS坐標(biāo)代碼增加的那2KB在Gzip后幾乎可以忽略不計。4.4 后續(xù)新增和修改圖標(biāo)的工作流接入ponytail之后日常迭代從原來的拼圖、量坐標(biāo)、改CSS、驗頁面變成兩步把新圖標(biāo)文件放進圖片目錄在CSS里正常引用然后重新構(gòu)建。構(gòu)建完雪碧圖自動更新、坐標(biāo)自動生成不需要人工介入。唯一要適應(yīng)的是新增任何小圖標(biāo)都會讓整張雪碧圖的contenthash變化導(dǎo)致所有圖標(biāo)在客戶端緩存同時失效。這在圖標(biāo)更新頻繁的業(yè)務(wù)里是個客觀代價。折中方案是按模塊拆多個雪碧圖比如icon-common、icon-order、icon-user讓小范圍改動只影響對應(yīng)模塊的雪碧圖緩存。5. 常見問題與排查技巧實錄接入過程中總有一些奇奇怪怪的問題下面把我在私下和團隊項目里碰到過的典型問題列成速查表方便你對照排查。5.1 問題速查表現(xiàn)象可能原因解決方向圖片沒被合并進雪碧圖limit太小、exclude誤傷、CSS里是動態(tài)路徑調(diào)大limit檢查include/exclude規(guī)則頁面圖標(biāo)錯亂或花屏同一張圖被多個CSS重復(fù)引用、background-size缺失保留坐標(biāo)唯一性確認background-size填的是整圖尺寸雪碧圖加載404publicPath配置錯誤、CSS引用路徑與實際輸出不符核對打包產(chǎn)物路徑手動訪問一下圖片URLCSS樣式覆蓋雪碧圖坐標(biāo)業(yè)務(wù)里后續(xù)又寫了background簡寫覆蓋了插件生成的position檢查選擇器優(yōu)先級必要時用獨立類名控制構(gòu)建報錯找不到圖片模塊圖片路徑在webpack resolve里解析不到檢查CSS里的相對路徑或配置plugin的上下文路徑雪碧圖過大limit設(shè)太高、把大素材混進來、排列算法利用率差調(diào)低limit正確排除大圖按模塊分包輸出JS動態(tài)插入的背景圖沒合并插件只掃描靜態(tài)CSS對動態(tài)樣式做改造或接受獨立加載5.2 我踩過的幾個坑說細節(jié)第一個坑是background簡寫屬性的覆蓋問題。ponytail生成的坐標(biāo)是直接追加在規(guī)則里的但如果業(yè)務(wù)代碼在這個樣式后面又用一個簡寫background: url(...) no-repeat center覆蓋坐標(biāo)會被重置。遇到這種情況我會調(diào)整一下選擇器的書寫位置或者干脆把簡寫里的坐標(biāo)部分去掉讓它繼承插件生成的background-position。第二個坑是Retina屏適配。合并后我用background-size寫的是整張雪碧圖的原始尺寸但在2x屏上如果不做處理圖標(biāo)會發(fā)虛。簡單方案是在媒體查詢里把background-size放大一倍或者讓設(shè)計師提供2x素材隨后在合并時統(tǒng)一處理。ponytail合并的是磁盤原圖它不會幫你自動生成2x版本這塊還是得靠性能和資源的取舍。第三個坑是CSS壓縮插件的處理順序。項目中如果同時使用css-minimizer-webpack-plugin要注意執(zhí)行順序。最好是讓ponytail先基于完整的CSS生成雪碧圖和坐標(biāo)隨后再做壓縮混淆順序反了的話插件可能掃描不到任何可用的url()引用結(jié)果就是什么都沒合并。第四個坑很隱蔽圖片路徑里有重復(fù)文件名。兩個不同目錄下都有icon-add.pngponytail默認按路徑區(qū)分圖片但如果某個步驟做了路徑歸一化兩張不同語義的圖可能被當(dāng)成同一張圖。排查時看到雪碧圖里缺少某個圖標(biāo)第一反應(yīng)應(yīng)該就是重復(fù)文件名問題。5.3 與webpack5、常用loader的兼容性注意事項如果你用的是webpack5建議先確認一下ponytail版本是否支持。有些老版本基于webpack4的hook編寫在webpack5里可能不生效。兼容性問題的表現(xiàn)通常不是報錯而是插件不執(zhí)行構(gòu)建靜默通過這對自動化工具來說比報錯更危險因為沒人注意到它沒干活。和less-loader、sass-loader混用時我建議讓ponytail處理的CSS是經(jīng)過預(yù)處理器編譯后的最終CSS。也就是說如果你在SCSS里寫background-image: url(...)沒問題less/sass會把url()原樣留在編譯結(jié)果里。但如果你在SCSS里用變量拼接路徑比如background-image: url(#{$img-path}/icon.png)編譯后變成url(/assets/img/icon.png)這時候要確認這個絕對路徑能被ponytail有效解析。我自己的習(xí)慣是配置里關(guān)閉preserve-url一類的選項讓路徑保持可解析狀態(tài)。6. 什么時候不應(yīng)該用ponytail講了這么多接入和收益最后也要把邊界說清楚。ponytail這類自動化雪碧圖插件不是所有項目都適合用。如果整個項目的圖片請求總數(shù)本來就很少比如一個單頁應(yīng)用只有五六個裝飾圖那么合并的收益很有限反而引入一個構(gòu)建依賴意義不大。如果項目大量使用矢量圖標(biāo)庫比如直接引入iconfont或SVG庫那也沒有雪碧圖的用武之地。還有一種情況是圖片本身會動態(tài)變化比如用戶上傳的縮略圖、活動運營臨時替換的Banner這些本來就該走獨立URL不能塞進構(gòu)建期的靜態(tài)雪碧圖。我個人的判斷標(biāo)準(zhǔn)是當(dāng)頁面里零散的小圖數(shù)量達到10個以上、且它們的變化頻率低于每周一次時ponytail的投入產(chǎn)出比最劃算。如果活動頁面圖標(biāo)每周都在變也能用但要做好“每次構(gòu)建都更新雪碧圖緩存”的心理準(zhǔn)備。我自己接入這個插件后最明顯的感受是把一項需要人工細心維護的活變成了構(gòu)建管線里一個不起眼的環(huán)節(jié)。團隊新成員不需要知道雪碧圖坐標(biāo)怎么量他們只需要正常寫CSS、正常引圖構(gòu)建完就自動處理好了。最后再分享一個小技巧接入完成后故意在CSS里新增一個臨時圖標(biāo)然后構(gòu)建如果產(chǎn)物里多出那張小圖且CSS坐標(biāo)正確說明整個自動鏈路是通的這個驗證方法比看配置、讀文檔都直觀我每次改造完都會跑一遍。