1889 字
9 分鐘

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 為什麼會同時讀取很多檔案#

網站原本使用這種掃描範圍:

tailwind.config.cjs
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 的位置解析它實際使用的套件:

Terminal window
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 留在專案外的比較目錄:

Terminal window
pnpm build
mkdir -p /tmp/site-css-before
cp dist/_astro/*.css /tmp/site-css-before/
# 套用修改後,再執行一次
pnpm build
mkdir -p /tmp/site-css-after
cp 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

Tailwind CSS 3.4.19:expandTailwindAtRules 原始碼

Vite 8 遷移文件:Rolldown

Node.js fs.globSync 文件

Cloudflare Workers 建置 EMFILE:Tailwind 3 的暫時處理方式
https://laplusda.com/posts/cloudflare-workers-build-emfile-tailwind/
作者
Zero
發佈於
2026-11-05
許可協議
CC BY-NC-SA 4.0