Cloudflare Workers 建置 EMFILE:Tailwind 3 的暫時處理方式
本文目錄
這次把 Astro 靜態網站部署到 Cloudflare Workers,依賴安裝完成,卻在產生靜態入口時停在這行:
[postcss] EMFILE: too many open files, open '/workspace/site/tailwind.config.cjs'目前採用的暫時處理方式,是在正式建置時先逐檔讀取 Tailwind 需要掃描的內容,再以 raw content 傳入 Tailwind 3。 修改後,同一個網站在 Cloudflare 完成建置與部署,修正當時產出的 CSS 也與修改前相同。
下面保留錯誤、版本與程式碼,專案名稱及路徑已改為示意值,帳號、網域、提交和建置識別碼均省略。
先確認 EMFILE 發生在哪個階段
這次失敗的環境是 Astro 7.3.8、Tailwind CSS 3.4.19、PostCSS 與 Node.js 24。Cloudflare 執行的是網站的靜態建置指令,之後才會由 Wrangler 上傳產物。
建置紀錄裡有幾種不同訊息:
| 訊息 | 這次的實際結果 |
|---|---|
Node.js url.parse() 棄用警告 | 依賴安裝繼續完成 |
| KaTeX 的 Unicode 警告 | Markdown 繼續處理 |
| 程式碼語言無法高亮 | 退回純文字後繼續建置 |
[postcss] EMFILE: too many open files | 靜態入口建置中止,指令以非零狀態結束 |
依 Node.js 的系統錯誤說明,EMFILE 表示程序可用的檔案描述符已耗盡。錯誤顯示 tailwind.config.cjs,能確定的是當下嘗試開啟這個檔案失敗;排查方向應涵蓋同一程序中其他尚未結束的檔案操作。
這也發生在 Workers Builds 的建置環境。若錯誤來自上線後的請求,例如 Workers Error 1102,應改查執行期的 CPU 與記憶體紀錄。
Tailwind 3 為什麼會同時讀取很多檔案
網站原本使用這種掃描範圍:
module.exports = { content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,mjs}'], // 其餘 theme、plugins 等設定保留}文章、元件與頁面都在 src,因此累積內容後,掃描會碰到數百個檔案。
檢查 Tailwind CSS 3.4.19 的 expandTailwindAtRules 原始碼,可以看到它將待處理內容切成每批最多 500 筆,再用 Promise.all 讀取其中的檔案。
這提供了一條可驗證的線索:把檔案內容預先準備好,能避開這段對來源檔案的平行讀取。 但當時沒有擷取雲端程序的檔案描述符占用量,仍無法指出是哪一組同時執行的工作剛好耗盡額度,也無法據此認定是 Cloudflare 平台缺陷。
先前想到的 Rollup 參數,還要核對實際建置器
maxParallelFileOps 是排查 Rollup 檔案並行時會遇到的設定。不過這個專案由 Astro 實際載入的是 Vite 8.3.4;Vite 8 已改用 Rolldown。因此,這次沒有把加上 Rollup 參數當成修正。
如果專案同時留下不同版本的 Vite,直接查看根目錄的 node_modules/vite 也可能看錯。可以從 Astro 的位置解析它實際使用的套件:
node --input-type=module -e 'import { createRequire } from "node:module";const requireFromAstro = createRequire(import.meta.resolve("astro"));console.log(requireFromAstro("vite/package.json").version);'升級框架後的環境盤點,可搭配 Astro 升級檢查一起做。
暫時處理:正式建置先讀內容,再交給 Tailwind
Tailwind 3 的 Content Configuration 文件支援直接傳入 { raw, extension }。這次沿用原本的檔案範圍,只調整正式建置的內容讀取方式。
下面是使用 postcss.config.mjs 的範例。它假設 tailwind.config.cjs 的 content 是由正向 glob 字串組成的陣列,建置指令從專案根目錄執行;Node.js 使用這次驗證的 24.x。
import { globSync, readFileSync } from 'node:fs'import { extname } from 'node:path'import autoprefixer from 'autoprefixer'import postcssImport from 'postcss-import'import tailwindcss from 'tailwindcss'import tailwindNesting from 'tailwindcss/nesting/index.js'import tailwindConfig from './tailwind.config.cjs'
const buildConfig = process.env.NODE_ENV === 'production' ? { ...tailwindConfig, content: [...globSync(tailwindConfig.content)].map((file) => ({ raw: readFileSync(file, 'utf8'), extension: extname(file).slice(1), })), } : undefined
export default { plugins: [ postcssImport(), tailwindNesting(), tailwindcss(buildConfig), autoprefixer(), ],}這裡有三個與排錯直接相關的細節:
readFileSync在這段迴圈中逐檔完成讀取,檔案文字交由 Tailwind 掃描。extension保留原檔案類型,讓 Tailwind 依類型選擇內容轉換方式,包含 Svelte 的處理。- 開發模式仍呼叫原本的
tailwindcss(),保留檔案追蹤與修改後更新的流程。
如果已有 content.files、自訂 transform、排除規則或 raw 內容物件,應先調整內容正規化方式,再套用這個範例。Node.js globSync 與原本 glob 工具的行為也要逐項核對,尤其是點開頭檔案、排除規則和符號連結。
這是 Tailwind 3 的處理方式本文沒有升級 Tailwind 4,也沒有變更平台的檔案上限。Tailwind 4 的整合與設定方式需要另外評估;這段 PostCSS 範例適用於本文的 Tailwind 3 專案。
怎麼確認這個暫時處理真的有效
這次使用三層驗證,各自回答不同問題。
用可控制的限制重現 Tailwind 讀檔失敗
在獨立的 Node.js 測試程序中,包住 fs.promises.readFile,當尚未完成的呼叫超過 64 個時回傳模擬的 EMFILE。
原本以檔案路徑交給 Tailwind 的方式會失敗;換成正式建置的 raw 內容後,相同測試可以產生 CSS。這證明改動避開了受測的平行讀檔路徑,64 是測試設定的門檻,雲端當時的實際上限則沒有量到。
整站也曾在本機把 shell 的開檔限制設為 256 後執行建置,但修改前後都成功,因此本機整站測試無法重現雲端失敗。
比對完整產物,確認掃描內容沒有遺漏
在文章內容保持相同的前提下,分別建置修改前後版本,比對所有 CSS 產物的檔名與雜湊。這次結果相同,48 項既有測試與網站 SEO 檢查也通過。
自己的專案可以先在兩個版本各執行一次建置,將 CSS 留在專案外的比較目錄:
pnpm buildmkdir -p /tmp/site-css-beforecp dist/_astro/*.css /tmp/site-css-before/
# 套用修改後,再執行一次pnpm buildmkdir -p /tmp/site-css-aftercp dist/_astro/*.css /tmp/site-css-after/diff -r /tmp/site-css-before /tmp/site-css-after使用前先確認兩個比較目錄是本次新建、沒有舊產物,並依專案調整 CSS 輸出路徑。若同時新增文章或修改 class,應先把內容變動分開,才能判斷 CSS 差異來自哪裡。
回到 Cloudflare,確認建置與部署都完成
推送修正後,Cloudflare 完成依賴安裝、靜態建置與部署,新版本顯示就緒;正式站首頁及一篇近期文章均回傳 HTTP 200。
本機建置成功、雲端建置成功和網站可以開啟,是不同層次的證據。之後修改部署流程時,也可以補上 Workers production build 整合測試,持續驗證路由與請求行為。
為什麼先保留為暫時處理
目前得到的結果是:這個專案調整 Tailwind 內容讀取方式後,該次 Cloudflare 部署成功。後續還需要觀察內容增加、依賴更新及重新建置後的結果。
預先讀取 raw 內容會把來源文字留在記憶體中,同步讀取也會阻塞建置程序的 JavaScript 執行。這次接受這個取捨,是為了先恢復部署;沒有把它當成記憶體或建置時間最佳化。
後續若調整 Tailwind 版本、掃描範圍或建置器,會重新跑相同的限制測試與 CSS 比對,再決定是否移除這段處理。先保留可回復的修改與驗證方式,才能知道下一次成功是修正仍有效,還是環境條件剛好不同。
參考資料:
Tailwind CSS 3 Content Configuration