如何提升基于Express、EJS的程序化图片发送速度?
我来帮你拆解下这个性能瓶颈,从几个核心方向入手优化,应该能大幅提升你的图片服务响应速度:
1. 把同步IO全部换成异步非阻塞操作
这是你当前性能慢的核心原因之一——Node.js是单线程事件循环模型,同步IO会直接阻塞整个进程,导致所有请求都得排队等待。比如你之前可能用了fs.existsSync()或者fs.readFileSync()这类同步方法,现在必须换成异步版本。
举个路由处理的例子,原来的同步代码可能是这样:
// 糟糕的同步写法 app.get('/images/:filename', (req, res) => { const filePath = path.join(__dirname, 'images', req.params.filename); if (fs.existsSync(filePath)) { res.sendFile(filePath); } else { res.sendFile(path.join(__dirname, 'images', 'default.png')); } });
改成异步+async/await的写法:
// 优化后的异步写法 app.get('/images/:filename', async (req, res) => { const filePath = path.join(__dirname, 'images', req.params.filename); try { await fs.promises.access(filePath, fs.constants.F_OK); res.sendFile(filePath); } catch (err) { // 文件不存在,返回通用图 res.sendFile(path.join(__dirname, 'images', 'default.png')); } });
2. 给图片判断逻辑加缓存层
每次请求都去磁盘查文件是否存在太浪费资源了,可以用内存缓存来记录已经验证过的图片路径,避免重复IO操作。推荐用lru-cache(一个轻量级的LRU缓存库),它会自动淘汰最近最少使用的缓存项,防止内存溢出。
示例代码:
const LRU = require('lru-cache'); const imageCache = new LRU({ max: 1000, ttl: 86400000 }); // 最多存1000条,缓存1天 app.get('/images/:filename', async (req, res) => { const filename = req.params.filename; // 先查缓存 const cacheResult = imageCache.get(filename); if (cacheResult) { return res.sendFile(cacheResult); } const filePath = path.join(__dirname, 'images', filename); try { await fs.promises.access(filePath, fs.constants.F_OK); imageCache.set(filename, filePath); res.sendFile(filePath); } catch (err) { const defaultPath = path.join(__dirname, 'images', 'default.png'); imageCache.set(filename, defaultPath); // 把不存在的也缓存起来 res.sendFile(defaultPath); } });
3. 优化EJS渲染效率
不要在EJS模板里做任何业务逻辑判断,提前在后端把所有图片的URL预处理好,模板只负责纯渲染。这样能减少模板编译和运行时的逻辑开销,同时让代码更清晰。
比如后端处理数据:
// 假设你有一个需要渲染的数组 const items = await getItemsFromDB(); // 从数据库取数据 // 预处理每个item的图片URL const processedItems = items.map(item => ({ ...item, imgUrl: `/images/${item.imageFilename}` // 直接生成图片服务的URL,不用在模板里判断 })); // 传给EJS渲染 res.render('items', { items: processedItems });
然后EJS模板里只需要简单循环:
<% items.forEach(item => { %> <div class="item"> <img src="<%= item.imgUrl %>" alt="<%= item.name %>"> <p><%= item.name %></p> </div> <% }) %>
另外,生产环境一定要开启EJS的模板缓存,避免每次请求都重新编译模板:
app.set('view cache', true); // 只在生产环境启用
4. 用Express内置的express.static托管静态图片
express.static是经过官方优化的静态文件中间件,比你自己写路由处理高效得多。它内置了缓存头处理、文件流传输等优化,还能自动处理304缓存验证。
配置示例:
// 托管真实图片目录,设置缓存头为1天 app.use('/images', express.static(path.join(__dirname, 'images'), { maxAge: '1d', // 浏览器缓存1天 etag: true, // 启用ETag验证,支持304 Not Modified })); // 处理所有/images/下的404请求,返回通用图 app.get('/images/*', (req, res) => { res.sendFile(path.join(__dirname, 'images', 'default.png'), { maxAge: '7d' // 通用图缓存时间可以更长 }); });
这种方式完全不用自己写文件存在性判断,Express会自动处理,性能提升非常明显。
5. 生产环境启用多核进程
Node.js单线程无法利用多核CPU,用cluster模块可以启动多个子进程,充分利用服务器的CPU资源,提升并发处理能力。
示例代码:
const cluster = require('cluster'); const os = require('os'); const numCPUs = os.cpus().length; if (cluster.isPrimary) { // 主进程负责fork子进程 console.log(`主进程 ${process.pid} 正在运行`); for (let i = 0; i < numCPUs; i++) { cluster.fork(); } // 子进程退出时重新fork cluster.on('exit', (worker, code, signal) => { console.log(`子进程 ${worker.process.pid} 退出,重新启动`); cluster.fork(); }); } else { // 子进程启动Express应用 const app = require('./app'); // 你的Express应用文件 app.listen(3000, () => { console.log(`子进程 ${process.pid} 监听端口3000`); }); }
6. 用流式传输图片(可选)
如果你需要自己处理图片(比如加水印、压缩),不要用sendFile,而是用流式传输,这样可以减少内存占用,提升响应速度。比如:
const fs = require('fs'); const path = require('path'); app.get('/images/:filename', (req, res) => { const filePath = path.join(__dirname, 'images', req.params.filename); const stream = fs.createReadStream(filePath); stream.on('error', () => { // 出错时切换到通用图的流 fs.createReadStream(path.join(__dirname, 'images', 'default.png')).pipe(res); }); stream.pipe(res); });
把这些优化点结合起来,你的服务性能应该会有质的提升。优先从express.static和异步IO这两点入手,这两个是最见效的优化。
内容的提问来源于stack exchange,提问作者user4893295

