perf: avoid await import for code that's guaranteed to execute#8086
perf: avoid await import for code that's guaranteed to execute#8086
await import for code that's guaranteed to execute#8086Conversation
|
await import for code that's guaranteed to execute
There was a problem hiding this comment.
Pull request overview
Performance-focused refactor aimed at reducing runtime overhead in Hardhat 3 by replacing hot-path dynamic imports/async cloning with synchronous equivalents.
Changes:
- Made
deepClonesynchronous by removing lazyawait import("rfdc")and updating call sites/tests accordingly. - Replaced an
await import(...)ofcreateHandlersArraywith a static import in the network hook handler. - Updated a few internal consumers to stop awaiting
deepClone.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/compilation-job.ts | Stops awaiting deepClone when building compiler input settings. |
| v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts | Converts a per-request dynamic import to a static import; stops awaiting deepClone. |
| v-next/hardhat/src/internal/builtin-plugins/network-manager/config-resolution.ts | Stops awaiting deepClone when copying default chain descriptors. |
| v-next/hardhat-utils/test/lang.ts | Updates deepClone tests to call the new synchronous API. |
| v-next/hardhat-utils/src/lang.ts | Changes deepClone from async to sync (public API surface). |
| v-next/hardhat-utils/src/internal/lang.ts | Switches rfdc to a static import and initializes the clone function eagerly. |
| import rfdc from "rfdc"; | ||
|
|
||
| import { isObject } from "../lang.js"; | ||
|
|
||
| let clone: ReturnType<typeof rfdcT> | null = null; | ||
| export async function getDeepCloneFunction(): Promise<<T>(input: T) => T> { | ||
| const { default: rfdc } = await import("rfdc"); | ||
|
|
||
| if (clone === null) { | ||
| clone = rfdc(); | ||
| } | ||
| const clone = rfdc(); | ||
|
|
||
| export function getDeepCloneFunction(): <T>(input: T) => T { | ||
| return clone; |
There was a problem hiding this comment.
getDeepCloneFunction now eagerly imports rfdc and initializes clone at module load (const clone = rfdc()). Because @nomicfoundation/hardhat-utils/lang is imported widely for unrelated helpers (e.g. isObject, sleep), this change makes every consumer pay the rfdc import/init cost even when deepClone is never called, which can regress startup performance and changes when failures would surface (module-load vs first-call).
Consider restoring lazy initialization while keeping deepClone synchronous, e.g. cache clone but only require("rfdc")/initialize it inside getDeepCloneFunction (using createRequire in ESM), or otherwise ensure this eager load is actually on a guaranteed-hot path.
| export function deepClone<T>(value: T): T { | ||
| return getDeepCloneFunction()<T>(value); | ||
| } |
There was a problem hiding this comment.
Changing deepClone from async (returning Promise<T>) to a synchronous function (returning T) is a breaking public API change for @nomicfoundation/hardhat-utils/lang. Any external consumers using promise chaining (e.g. deepClone(x).then(...)) or typed as Promise will break.
If the sync behavior is required for perf, consider introducing a new sync API (and keeping the existing async deepClone as a thin wrapper for backwards compatibility), or ensure this is released with an appropriate major-version bump and explicit migration notes.
| import type { HardhatEthersProvider } from "../hardhat-ethers-provider/hardhat-ethers-provider.js"; | ||
| import type { HardhatEthersSigner } from "../signers/signers.js"; | ||
| import { HardhatEthersSigner } from "../signers/signers.js"; | ||
| import type { ethers as EthersT } from "ethers"; |
There was a problem hiding this comment.
Switching from a dynamic import to a static import here means the signer implementation (and its transitive deps like ethers) will be loaded as soon as this module is evaluated, even in flows that never call getSigner/getSigners. If the goal is to remove per-call await import(...) overhead but keep lazy loading, consider caching the imported module/class the first time getSigner is called (lazy + cached) rather than importing it unconditionally at top-level.
|
This was superseded by #8088, based on your findings and ideas. Excellent job! 🔥 |
Performance optimisations for Hardhat 3 by moving hot code
await imports that are guaranteed to be run to be synchronous.In the OpenZeppelin contracts repo, this reduced the Mocha test suite runtime from 284.57s to 136.92s (-51.9%).