ข้ามไปยังเนื้อหา

File System

Node มาพร้อม filesystem operations เดียวกันใน API สามแบบที่แตกต่างกัน:

รูปแบบImportใช้เมื่อไหร่
Callback (async)import fs from 'node:fs'code เก่า, Node streams
Synchronousimport fs from 'node:fs'CLI scripts, config ตอน startup
Promise (async)import fs from 'node:fs/promises'server code ทั้งหมดที่เขียนใหม่
import fs from 'node:fs';
fs.writeFileSync('./hello.txt', 'Hello from Node.js!');
fs.readFile('./hello.txt', 'utf8', function(err, data) {
if (err) throw err;
console.log(data);
fs.unlinkSync('./hello.txt');
});
Node.js

Needs the Node.js runtime — open in StackBlitz to run.

writeFile สร้างไฟล์ถ้ายังไม่มี และ แทนที่ เนื้อหาถ้ามีอยู่แล้ว ใช้ appendFile เพื่อเพิ่มต่อจากไฟล์ที่มีอยู่

Node.js

Needs the Node.js runtime — open in StackBlitz to run.

Node.js

Needs the Node.js runtime — open in StackBlitz to run.

readFileSync ไม่ได้แค่ “อ่านไฟล์ช้ากว่า” — มัน รันจนจบก่อนที่สิ่งอื่นใดใน process จะรันได้ รวมถึง timer ที่ถูกตั้งไว้แล้วด้วย delay 0 ms readFile (แบบ promise) ส่งงานอ่านออกไปแล้ว return ทันที ปล่อยให้ code synchronous ที่เหลือ — และ event loop — เดินหน้าต่อไปได้ในขณะที่การอ่านเกิดขึ้นเบื้องหลัง:

Node.js
Output (verified on Node 24)
sync read length: 5000000
timer fired before readFileSync returned: false
next line ran before the read settled

readFile() (sync หรือ promise) จอง buffer หนึ่งก้อนที่เก็บทั้งไฟล์ สำหรับไฟล์ขนาด 50 MB นั่นหมายถึงการจอง 50 MB หนึ่งครั้ง เก็บไว้ใน memory จนกว่าคุณจะใช้เสร็จ stream กลับส่งมอบไฟล์เป็น chunk ที่มีขอบเขต — ดีฟอลต์ไม่เกิน highWaterMark (64 KiB) ต่อครั้ง — ดังนั้นการใช้ memory จึงคงที่โดยประมาณไม่ว่าไฟล์จะขนาดเท่าไหร่:

Node.js
Output (verified on Node 24)
readFile() loaded bytes in one buffer: 52428800
stream delivered bytes across 800 chunks
largest single chunk (bytes): 65536
total bytes match: true

สำหรับไฟล์ 50 MB readFile ต้องการการจอง 50 MB ที่ต่อเนื่องกันหนึ่งก้อน; stream ไม่เคยถือมากกว่า 64 KiB ต่อครั้ง — ความแตกต่างที่สำคัญเมื่อไฟล์อาจมีขนาดหลายกิกะไบต์ หรือเมื่อ request หลายร้อยตัวกำลังอ่านไฟล์พร้อมกัน

API ใหม่สองตัวช่วยปลด dependency เก่า: fs.glob (stable ใน Node 22+) แทน package glob และ file handle รองรับ await using (Node 24+) จึงปิดตัวเองได้:

import { writeFileSync, unlinkSync } from 'node:fs';
import { glob, open } from 'node:fs/promises';
writeFileSync('./config.json', JSON.stringify({ port: 3000 })); // demo fixture
// Find files without the `glob` package
for await (const file of glob('src/**/*.test.js')) {
console.log(file);
}
// The handle is closed when the block exits — even if it throws
{
await using fh = await open('./config.json');
const text = await fh.readFile('utf8');
console.log(JSON.parse(text).port);
}
unlinkSync('./config.json');
import { readFileSync, writeFileSync } from 'node:fs';
writeFileSync('./config.json', JSON.stringify({ port: 3000 })); // demo fixture
// Acceptable at startup — process has not yet started accepting requests
const config = JSON.parse(readFileSync('./config.json', 'utf8'));
console.log(config.port);

อย่าเรียก readFileSync ใน request handler, route callback หรือ function ใดที่ถูกเรียกหลังจาก server เริ่มทำงาน

flowchart TB
  Call["fs call"] --> Sync["readFileSync / writeFileSync"]
  Call --> Async["readFile / writeFile (fs/promises)"]
  Sync -->|"runs to completion, no one else can run"| Blocks["blocks the event loop — every other request waits"]
  Async -->|"handed to the threadpool, returns a pending Promise"| Free["event loop keeps handling other work"]
  Blocks -->|"acceptable"| Startup["CLI scripts / before server.listen()"]
  Free -->|"required"| Server["server request handlers"]
fs call แบบ sync รันจนจบและ block ทุกอย่าง; fs call แบบ async ส่งงานไปที่ threadpool แล้ว return ทันที
ทางเลือกข้อดีต้นทุน
fs sync (readFileSync)code ง่ายที่สุด — ไม่มี callback ไม่มี await ไม่มีการไป-กลับกับ threadpoolblock ทั้ง process ตลอดระยะเวลาของ call — ยอมรับไม่ได้เมื่อ server เริ่มรับ request แล้ว
fs/promises (readFile)ไม่เคย block event loop; ประกอบกับ async/await ได้มีการไป-กลับกับ threadpool เพิ่มอีกหนึ่งครั้งต่อ call — latency สูงกว่า sync เล็กน้อยสำหรับ call เดี่ยวใน script ที่ไม่มีการทำงานพร้อมกัน
readFile() ทั้งไฟล์code ง่ายที่สุดเท่าที่จะทำได้ — buffer เดียว ไม่ต้อง wire eventการใช้ memory scale ตามขนาดไฟล์; ไฟล์ 1 GB ต้องการ buffer 1 GB
createReadStream()memory คงที่ไม่ว่าไฟล์ขนาดเท่าไหร่; เริ่มประมวลผลได้ก่อนที่ไฟล์จะอ่านเสร็จcode มากกว่า — คุณต้องจัดการ 'data'/'end'/'error' เอง (หรือใช้ pipeline ที่ครอบคลุมในโมดูล streams)
  • “readFileSync ก็แค่ readFile ที่ช้ากว่า” — ความจริง: ความแตกต่างไม่ใช่ความเร็ว แต่คือ readFileSync block ทั้ง process — ไม่มี timer, ไม่มี request อื่น, ไม่มี I/O callback ไหนทำงานได้จนกว่ามันจะ return
  • “readFileSync แบบเร็ว ๆ ไม่เป็นไรเพราะใช้แค่ไม่กี่มิลลิวินาที” — ความจริง: มิลลิวินาทีเหล่านั้นถูกขโมยไปจากทุก request ที่ server กำลังจัดการอยู่ ณ ขณะนั้น ไม่ใช่แค่ตัวที่ทำให้เกิดการอ่าน
  • “readFile() กับ stream ใช้ memory พอ ๆ กันเพราะอ่านไฟล์เดียวกัน” — ความจริง: readFile() ถือทั้งไฟล์เป็น buffer เดียว; stream ถือ chunk highWaterMark (ดีฟอลต์ 64 KiB) อย่างมากหนึ่งอันต่อครั้ง ไม่ขึ้นกับขนาดไฟล์ทั้งหมด
  • route handler หรือ function ใดก็ตามที่ถูกเรียกหลัง server.listen() ใช้ readFileSync, writeFileSync หรือ existsSync หรือเปล่า? ตัวไหนก็ตามในนี้ block ทุก request ที่กำลังทำงานพร้อมกันตลอดระยะเวลาของ call
  • code โหลดไฟล์ที่อาจมีขนาดใหญ่หรือมาจากผู้ใช้ด้วย readFile() เข้า buffer เดียว แทนที่จะ stream มัน ทั้งที่ไฟล์จะถูกส่งต่อหรือประมวลผลทีละ chunk อยู่แล้วหรือเปล่า?
  • file handle ที่เปิดด้วย fs.open()/fsPromises.open() ถูกปิดในทุกเส้นทางที่ออก รวมถึง error ที่ถูก throw หรือเปล่า — หรือ await using (Node 24+) จะทำให้มันเกิดขึ้นอัตโนมัติได้?

🐛 หา bug ในโค้ดนี้

import { readFileSync } from 'node:fs';
import http from 'node:http';
var server = http.createServer(function(req, res) {
// reads the config file from disk on every single request
var config = JSON.parse(readFileSync('./config.json', 'utf8'));
res.end(JSON.stringify({ port: config.port }));
});
server.listen(3000);
เฉลย

readFileSync รันภายใน request handler ดังนั้นทุก request ที่เข้ามา block ทั้ง event loop ตลอดระยะเวลาของการอ่านดิสก์ — ไม่มี request อื่นถูกรับหรือตอบกลับได้ในขณะที่มันทำงานอยู่ แม้แต่ตัวที่ไม่เกี่ยวข้องกับ route นี้เลย วิธีแก้คืออ่าน config ครั้งเดียวตอน startup (ก่อน server.listen() ที่ sync call ยอมรับได้) แล้วใช้ object ที่ parse แล้วซ้ำจากตัวแปรในทุก request หรือใช้ readFile ของ fs/promises พร้อม in-memory cache เล็ก ๆ ถ้าไฟล์เปลี่ยนได้ตอน runtime

ทำไมควรหลีกเลี่ยง fs.readFileSync ใน server request handler?
fs.writeFile ทำอะไรถ้าไฟล์ปลายทางมีอยู่แล้ว?
import ใดที่ให้ promise-based fs API?
โค้ดนี้พิมพ์อะไร? var timerFired = false; setTimeout(() => { timerFired = true; }, 0); var data = readFileSync(path, "utf8"); console.log(timerFired);
ไฟล์ขนาด 200 MB ถูกอ่านครั้งหนึ่งด้วย fs.readFile() และอีกครั้งด้วย fs.createReadStream() ข้อความไหนเกี่ยวกับการใช้ memory ถูกต้อง?
มีอะไรผิดปกติกับการเรียก readFileSync('./config.json') ภายใน HTTP request handler ที่รันทุก request?