阿里云主机折上折
  • 微信号
Current Site:Index > Top-level await

Top-level await

Author:Chuan Chen 阅读数:7994人阅读 分类: JavaScript

Basic Concepts of Top-Level Await

ECMAScript 12 introduced the top-level await feature, allowing the direct use of the await keyword at the module level. Previously, await could only be used inside async functions, which required additional wrapper functions for module initialization. Top-level await removes this restriction, making the organization of asynchronous code more intuitive.

// Previous approach
(async function() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  console.log(data);
})();

// With top-level await
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);

How It Works and Execution Order

Top-level await pauses module execution until the awaited Promise is resolved. This affects the construction of the module dependency graph:

  1. The module loader first resolves all static imports.
  2. It then executes the module code until the first await is encountered.
  3. The current module's execution is paused, and the loader proceeds to load dependent modules.
  4. Once the awaited Promise is resolved, the remaining code is executed.
// moduleA.mjs
console.log('Module A starts');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Module A continues');

// moduleB.mjs
import './moduleA.mjs';
console.log('Module B runs');

The execution order will be:

  1. Module A starts
  2. Module B runs
  3. (After 1 second) Module A continues

Practical Use Cases

Dynamic Module Imports

Top-level await is ideal for scenarios requiring conditional dynamic module imports:

const locale = navigator.language;
const messages = await import(`./locales/${locale}.mjs`);
console.log(messages.hello);

Configuration Initialization

You can wait for configuration loading at the module level:

const config = await (await fetch('/config.json')).json();

export function getApiUrl() {
  return config.apiUrl;
}

Resource Preloading

const [userData, productList] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/products').then(r => r.json())
]);

export { userData, productList };

Error Handling Mechanisms

Error handling for top-level await requires wrapping with try/catch:

try {
  const data = await fetchUnreliableApi();
  console.log(data);
} catch (error) {
  console.error('Failed to load data:', error);
  // Fallback values can be set
  const data = getFallbackData();
}

Impact on Module Dependencies

Top-level await changes module resolution order:

// a.mjs
console.log('a1');
await 0;
console.log('a2');

// b.mjs
import './a.mjs';
console.log('b1');

Output order will be: a1 → b1 → a2

Browser and Node.js Support

Modern browsers and Node.js support top-level await:

  • Chrome 89+
  • Firefox 89+
  • Safari 15+
  • Node.js 14.8+ (requires ES modules, file extension .mjs or "type": "module" in package.json)

Performance Considerations

When using top-level await, note:

  1. It may delay module availability.
  2. Overuse can slow down application startup.
  3. Suitable for initializing critical resources but not for frequently called logic.
// Not recommended - re-waits on every import
export const data = await fetchData();

// Recommended - waits only once
const _data = await fetchData();
export function getData() { return _data; }

Interoperability with CommonJS

In Node.js, top-level await works only in ES modules. CommonJS modules require wrapping with an async function:

// Equivalent in CommonJS
async function main() {
  const data = await fetchData();
  module.exports = { data };
}
main();

Debugging Tips

When debugging top-level await code:

  1. Browser developer tools now support direct debugging of module-level await.
  2. Add logs before and after await statements.
  3. Use Promise state inspection tools.
console.time('API load');
const data = await fetchData();
console.timeEnd('API load');

Combined Usage Example

Example combining top-level await with other ES features:

// Configuration loading module
const configPromise = fetch('/config').then(r => r.json());

// Wait for configuration to load
export const config = await configPromise;

// Initialize services based on configuration
export const apiClient = createApiClient(config.apiUrl);

// Load other resources in parallel
export const [user, products] = await Promise.all([
  fetch(`${config.apiUrl}/user`).then(r => r.json()),
  fetch(`${config.apiUrl}/products`).then(r => r.json())
]);

Limitations and Considerations

  1. Cannot be used in non-module scripts.
  2. Cannot be used directly inside functions or classes.
  3. Affects static analysis of modules.
  4. Circular dependencies may cause deadlocks.
// Incorrect example - cannot use directly inside functions
function getData() {
  const data = await fetchData(); // SyntaxError
  return data;
}

Usage with TypeScript

TypeScript 3.8+ supports top-level await with tsconfig.json configuration:

{
  "compilerOptions": {
    "module": "esnext",
    "target": "es2017"
  }
}

Usage example:

interface Config {
  apiUrl: string;
  timeout: number;
}

const config: Config = await fetch('/config').then(r => r.json());

Integration with Build Tools

Major build tools support top-level await:

  • webpack 5+ supports it natively.
  • Rollup requires the @rollup/plugin-commonjs plugin.
  • Vite supports it natively.
  • Parcel 2+ supports it natively.

webpack configuration example:

// webpack.config.js
module.exports = {
  experiments: {
    topLevelAwait: true
  }
};

Testing Strategies

When testing modules using top-level await:

  1. Use a test runner that supports ES modules (Jest 27+).
  2. Additional configuration may be required.
  3. Consider using dynamic mocks.
// __mocks__/api.js
export const fetchData = jest.fn(() => Promise.resolve({ data: 'mock' }));

// test.js
import { fetchData } from '../api';
import { processedData } from '../moduleUsingTopLevelAwait';

describe('module tests', () => {
  beforeAll(async () => {
    await processedData; // Wait for top-level await to complete
  });

  test('should process data', () => {
    expect(processedData).toEqual({ processed: true });
  });
});

Combining with Async Iterators

Top-level await can be used with async iterators:

async function* asyncData() {
  yield await fetch('/data/1');
  yield await fetch('/data/2');
}

const dataIterator = asyncData();
const firstItem = await dataIterator.next();

Dynamic Polyfill Loading

Load polyfills dynamically based on feature detection:

if (!('IntersectionObserver' in window)) {
  await import('intersection-observer-polyfill');
}

// Safely use IntersectionObserver
const observer = new IntersectionObserver(callback);

Application in Server-Side Rendering (SSR)

Using top-level await in SSR frameworks:

// Data fetching module
export const serverData = await fetchServerData();

// Page component
export default function Page({ data }) {
  return <div>{data.content}</div>;
}

// During SSR rendering
export async function getServerSideProps() {
  return { props: { data: serverData } };
}

Integration with Web Workers

Using top-level await in Worker modules:

// worker.mjs
const heavyData = await processInBackground();

self.onmessage = ({ data }) => {
  // Use preprocessed data
  const result = heavyData[data.key];
  self.postMessage(result);
};

Code Splitting Optimization

Combine with dynamic imports for smart code splitting:

const userModule = await import(
  isAdmin ? './adminPanel.js' : './userPanel.js'
);

// Initialize the corresponding module
userModule.initialize();

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.