
ESLint v9.34.0 引入了多线程检测,这是一个已经开发了十多年的功能。通过创建多个工作线程,ESLint 现在可以同时处理多个文件,大大减少了大型项目的检测时间。
🌐 ESLint v9.34.0 introduces multithread linting, concluding a feature that’s been in the making for over ten years. By spawning several worker threads, ESLint can now process multiple files at the same time, dramatically reducing lint times for large projects.
在拥有多个 CPU 核心和快速存储的机器上,这一更改可能会带来明显的差异——尤其是在你对数百或数千个文件进行 lint 时。我们的早期测试者已经看到了大约 1.30x 的加速作为起点,而有一位甚至报告了 3.01x 的提升。
🌐 On machines with multiple CPU cores and fast storage, this change can make a noticeable difference — especially when you’re linting hundreds or thousands of files. Our early testers have seen a speedup of around 1.30x as a starting point, while one even reported a 3.01x improvement.
历史
🌐 History
将 linting 过程并行化的想法已经存在十多年了,早在 Node.js 提供内置工作线程之前就有了。早期的讨论主要集中在一些基本问题上:应使用哪些工具进行并行化,范围应扩展到多大,以及需要进行哪些架构上的更改才能实现这一目标。人们还担心潜在的缺点——额外的维护负担,如果默认启用并行化可能会导致只有少量文件的项目变慢,以及阻碍其他理想功能实现的可能性,例如基于项目的 linting,这些功能要求 ESLint 即使在单独 lint 文件时也能理解多个文件之间的关系。
🌐 The idea of parallelizing the linting process has been around for well over a decade, long before Node.js offered built-in worker threads. Early discussions wrestled with fundamental questions: which tools should be used for parallelization, how far should the scope extend, and what architectural changes would be required to make it possible. There were also concerns about the potential downsides — the extra maintenance burden, the risk of slowing down projects with only a handful of files if parallelization were enabled by default, and the possibility of blocking other desirable features such as project-based linting, which would require ESLint to understand relationships between multiple files even when linting them individually.
与此同时,其他工具不断前进。像 AVA、Mocha 和 Jest 这样的测试运行器开始提供内置的并行执行功能,而富有创意的开发者也找到了使用封装器和外部脚本并行运行 ESLint 的方法。这些尝试证明了并行 linting 的价值,但也突显了从外部添加该功能的局限性。最终,使多线程 linting 真正无缝的唯一方法是将其直接集成到 ESLint 的核心中。
🌐 In the meantime, other tools forged ahead. Test runners like AVA, Mocha, and Jest began to ship with built-in parallel execution, and inventive developers found ways to run ESLint in parallel using wrappers and external scripts. These experiments proved that parallel linting could be valuable, but they also highlighted the limitations of bolting it on from the outside. Ultimately, the only way to make multithread linting truly seamless was to integrate it directly into ESLint’s core.
挑战
🌐 Challenges
将这个长期存在的想法转化为一个实用且用户友好的功能绝非易事。第一步是收集过去讨论的众多线索——尤其是 issue #3565 中的提案和辩论——并将它们编织成一个连贯的设计,使其能够在 CLI 和 Node.js API 中同样有效。目标是在对现有用户干扰最小的情况下引入多线程,因此唯一显著的变化将是更快的构建速度和略微升温的处理器。同时,我们希望有一个自动 (auto) 并发模式,在大多数情况下能够选择合理的默认设置,并且提供一种方式在用户所选择的设置可能实际上降低性能时发出警告。
🌐 Turning that long-standing idea into a practical, user-friendly feature was far from trivial. The first step was to gather the many threads of past conversations — most notably the proposals and debates in issue #3565 — and weave them into a coherent design that could work equally well from the CLI and the Node.js API. The goal was to introduce multithreading with as little disruption as possible for existing users, so the only noticeable change would be faster builds and slightly warmer processors. Alongside that, we wanted an automatic (auto) concurrency mode that could choose sensible defaults in most situations, as well as a way to warn users when their chosen settings might actually be slowing things down.
克服可克隆性限制
🌐 Overcoming Cloneability Constraints
技术障碍很快出现。其中最主要的是要求传递给工作线程的所有选项必须可克隆,这一限制排除了某些常见模式,例如直接传递函数或复杂的插件对象。最终,这一限制促使了选项模块的创建,通过让每个工作线程导入相同的模块,而不是接收选项的克隆副本,从而完全绕开了可克隆性问题。
🌐 Technical hurdles soon followed. Chief among them was the requirement that all options passed to worker threads be cloneable, a restriction that ruled out certain common patterns such as passing functions or complex plugin objects directly. This limitation ultimately inspired the creation of options modules, which sidestep the cloneability problem entirely by letting each worker import the same module rather than receiving a cloned copy of the options.
让 auto 无缝运行
🌐 Making auto Work Seamlessly
auto 模式带来了自己的一系列挑战。为了选择合适的线程数,ESLint 需要知道它将要检查的文件数量 —— 这一信息只有在文件枚举之后才能获取。这意味着需要重构代码,以便在线程创建之前、在枚举文件之后计算线程数,这是外部封装器无法干净地完成的事情。
🌐 The auto mode brought its own set of challenges. To pick the right number of threads, ESLint needed to know how many files it would be linting — information that only becomes available after file enumeration. This meant refactoring the code so that the thread count could be calculated after enumerating files and before creating threads, something an external wrapper can’t accomplish cleanly.
检测次优并发
🌐 Detecting Suboptimal Concurrency
最后,还有一个问题是,当用户的并发设置实际上影响性能时,如何提醒他们。我们的方法是测量不同线程中不同操作的持续时间,比较结果,并寻找暗示性能下降的模式。这并不完美——而且我们可能希望随着时间推移加以改进——但它为帮助用户充分利用多线程 lint 提供了一个起点。
🌐 Finally, there was the question of how to warn users when their concurrency setting was actually hurting performance. Our approach was to measure the duration of different operations across threads, compare the results, and look for patterns that suggested a slowdown. It’s not perfect — and we may want to refine it over time — but it gives us a starting point for helping users get the best out of multithread linting.
运作方式
🌐 How It Works
在某种意义上,得益于 Node.js 的事件循环架构,ESLint 多年来一直在进行“并行”代码检查,这种架构允许异步任务——例如从磁盘读取文件——同时运行。然而,CPU 密集型的工作如解析文件和应用规则一直是同步的,这意味着一次只能处理一个文件。
🌐 In a sense, ESLint has been doing “parallel” linting for years, thanks to Node.js’s event-loop architecture, which allows asynchronous tasks — like reading files from disk — to run concurrently. However, CPU-intensive work such as parsing files and applying rules has always been synchronous, meaning only one file could be processed at a time.
在一次典型的运行中,文件 I/O 只占总时间的一小部分;大部分时间用于解析和规则执行。这些任务无法通过单个线程并行化。多线程 linting 改变了这一点。每个工作线程一次处理一个文件,但多个线程可以同时工作。当所有线程完成时,它们的结果会在控制线程中收集并一起返回。
🌐 In a typical run, file I/O is a small fraction of the total time; most of it is spent on parsing and rule execution. These tasks cannot be parallelized with a single thread. Multithread linting changes that. Each worker thread processes one file at a time, but multiple threads can work simultaneously. When all threads finish, their results are gathered in the controlling thread and returned together.
CLI 多线程支持
🌐 CLI Multithreading Support
多线程通过新的 --concurrency 标志启用。你可以通过几种不同的方式设置它:
🌐 Multithreading is enabled through the new --concurrency flag. You can set it in a few different ways:
- 一个正整数(例如,
4)告诉 ESLint 使用的最大线程数。它永远不会生成超过要检查的文件数量的线程。 auto让 ESLint 根据你的 CPU 和文件数量决定线程数。对于足够大的项目,这种启发式方法默认使用报告的 CPU 数量的一半。off(默认)禁用多线程,与--concurrency=1等效。
当在具有快速 I/O 的多核机器上对许多文件进行 lint 时,收益最大。也就是说,如果你的配置或插件初始化时间很长,使用过多线程实际上可能会使速度变慢。
🌐 The biggest gains come when linting many files on a multi-core machine with fast I/O. That said, if your configuration or plugins take a long time to initialize, using too many threads can actually slow things down.
auto 并发的局限性
🌐 Limitations of auto Concurrency
根据设计,对于足够大的项目,--concurrency=auto 会将线程数计算为可用 CPU 内核数量的一半。在许多情况下,这个设置正好达到最佳效果。
🌐 By design, for sufficiently large projects, --concurrency=auto calculates the thread count as half of the number of available CPU cores. In many cases, this setting hits the sweet spot.
然而,硬件各不相同。Node.js 无法可靠地区分物理核心和逻辑(超线程)核心,也无法区分高性能核心和效率优化核心,因此 auto 启发式方法有时可能会低估或高估。
🌐 However, hardware varies. Node.js can’t reliably tell physical from logical (hyperthreaded) cores or high-performance from efficiency-optimized cores, so the auto heuristic can sometimes under- or overshoot.
如果提前知道目标机器和项目规模,最好尝试不同的数字 --concurrency 值(2、3、4 等),然后选择最快的。
🌐 If the target machine and project size are known in advance, it’s better to try out different numeric --concurrency values (2, 3, 4, etc.) and choose the fastest.
展望未来,我们可能会探索改进 auto 启发式的方法,以更好地考虑现实硬件的细微差别。
🌐 Looking forward, we may explore ways to improve the auto heuristic to better take into account the nuances of real-world hardware.
Node.js API 支持
🌐 Node.js API Support
如果你正在使用 Node.js API,通过 ESLint 构造函数中的新 concurrency 选项可以进行多线程 linting。可接受的值——数字、"auto" 和 "off"——与它们在 CLI 中的作用完全相同。
🌐 If you’re using the Node.js API, multithread linting is available via the new concurrency option in the ESLint constructor. The accepted values — numbers, "auto", and "off" — work exactly as they do in the CLI.
可克隆性要求
🌐 Cloneability Requirement
当启用并发时,ESLint 使用结构化克隆算法将选项传递给工作线程。这意味着只有可克隆的值——例如原始值、普通对象和数组——可以被传递。函数和自定义类实例不可克隆。
🌐 When concurrency is enabled, ESLint passes options to worker threads using the structured clone algorithm. This means only cloneable values — such as primitives, plain objects, and arrays — can be sent. Functions and custom class instances are not cloneable.
一些选项,如 plugin、baseConfig 和 overrideConfig,可以包含任意值,而其他选项,如 fix 和 ruleFilter,可以是函数。如果在启用并发时设置了不可克隆的值,ESLint 将抛出错误:
🌐 Some options, like plugin, baseConfig, and overrideConfig, can contain arbitrary values, while others, like fix and ruleFilter, can be functions. If you set an uncloneable value while concurrency is enabled, ESLint will throw an error:
选项“ruleFilter”无法被克隆。当启用并发时,所有选项必须是可克隆的。请删除不可克隆的选项或使用选项模块。
使用选项模块
🌐 Using Options Modules
为了绕过可克隆性限制,你可以在一个 ECMAScript 模块中定义你的 ESLint 选项:
🌐 To work around cloneability limits, you can define your ESLint options in an ECMAScript module:
// eslint-options.js
import config from "./my-eslint-config.js";
export default {
concurrency: "auto",
overrideConfig: config, // may include non-cloneable values
overrideConfigFile: true,
stats: true,
};
然后使用ESLint.fromOptionsModule()创建一个实例:
🌐 Then create an instance with ESLint.fromOptionsModule():
const optionsURL = new URL("./eslint-options.js", import.meta.url);
const eslint = await ESLint.fromOptionsModule(optionsURL);
在多线程模式下,每个工作线程导入相同的模块,而不是接收一个克隆的副本,因此可克隆性不再是问题。
🌐 In multithread mode, each worker imports the same module rather than receiving a cloned copy, so cloneability is no longer an issue.
你甚至可以使用数据 URL 内联你的选项模块:
🌐 You can even inline your options module using a data URL:
const configURL = new URL("./my-eslint.config.js", import.meta.url).href;
const optionsModuleText = `
import config from ${JSON.stringify(configURL)};
export default {
concurrency: "auto",
overrideConfig: config,
overrideConfigFile: true,
stats: true,
};
`;
const optionsURL = new URL(`data:text/javascript,${encodeURIComponent(optionsModuleText)}`);
const eslint = await ESLint.fromOptionsModule(optionsURL);
ESLint.fromOptionsModule() 可与任何工作线程可访问的 URL 类型一起使用。虽然它是为多线程代码检查设计的,但选项模块是一个独立的功能,即使没有并发也可以使用。
更多提示
🌐 Further Tips
通过遵循以下实用技巧来增强代码检查性能:
🌐 Enhance linting performance by following these practical tips:
1. 测试你的设置
🌐 1. Benchmark Your Setup
使用不同的 --concurrency 设置测量 linting 时间。例如,可以像这样使用 hyperfine:
🌐 Measure linting times with different --concurrency settings. For example, use hyperfine like this:
hyperfine --parameter-list concurrency off,2,3,4 "node node_modules/eslint/bin/eslint --concurrency {concurrency}"
这对于各种线程数产生了可比的结果。
🌐 This yields comparable results for various thread counts.
2. 调整每台机器的并发数
🌐 2. Adjust Concurrency per Machine
- 如果可行,尝试在每台机器上使用不同的数字
--concurrency值,而不是依赖auto。 - 从你机器物理核心的一半开始,然后测试更高或更低的值。
- 包含
--concurrency=off以查看单线程运行的性能。
3. 利用缓存
🌐 3. Leverage Caching
将 --cache 与 --concurrency 一起使用——或替代 --concurrency——以加快增量运行速度。
🌐 Use --cache alongside—or in place of—--concurrency to speed up incremental runs.
4. CI 和容器环境
🌐 4. CI and Container Environments
请注意,CI 运行器和容器可能会限制资源或使用虚拟化的 CPU 内核。在这些环境中重新进行基准测试,以验证多线程的 linting 是否带来了显著的性能提升。
🌐 Be aware that CI runners and containers may throttle resources or use virtualized CPU cores. Re-benchmark in those environments to verify whether multithread linting delivers a meaningful performance improvement.
结论
🌐 Conclusion
将多线程 linting 付诸实践是一个协作的努力,它由 ESLint 团队和更广泛社区的想法、反馈和测试共同塑造。我们感谢所有参与将这一概念变为现实的人员。
🌐 Bringing multithread linting to life was a collaborative effort, shaped by the ideas, feedback, and testing of both the ESLint team and the wider community. We are thankful to everyone involved in the process of making this concept become a reality.
我们知道,引入多线程代码检查不会没有小问题。一些工具和配置一开始可能无法很好配合。随着用户在未来采用它,我们预计会听到一些问题反馈,并且我们承诺会快速修复这些问题。
🌐 We know that introducing multithread linting won’t be without its hiccups. Some tools and configurations may not play nicely at first. As users adopt it in the future, we expect to hear about issues, and we’re committed to fixing them quickly.
