Skip to main content

Command Palette

Search for a command to run...

📦 How JavaScript Imports Actually Work, A Deep Dive for Devs Who Love to Know "Why"

Published
4 min read
📦 How JavaScript Imports Actually Work, A Deep Dive for Devs Who Love to Know "Why"
A

Full-Stack Developer & Tech Writer specializing in Python (Django, FastAPI, Flask) and JavaScript (React, Next.js, Node.js). I build fast, scalable web apps and share practical insights on backend architecture, frontend performance, APIs, and Web3 integration. Available for freelance and remote roles.

If you’ve been writing JavaScript for a while, you’ve probably typed:

import { readFile } from "fs";

…and moved on with your day.

But have you ever stopped and thought:
“Wait. Where does this readFile even come from?”

Under the hood, your JS engine (V8, SpiderMonkey, JavaScriptCore…) is doing way more than just “copy-pasting some code.”

It’s building module graphs, parsing ASTs, linking live bindings, and caching results all before your code runs.

Let’s lift the hood and see what’s really going on.
You’ll come away writing faster, cleaner, and more predictable code.

🧩 So, What Is a Module Really?

A module isn’t just “a file.”

It’s a sealed box with:

  • Its own scope (no global leaks 🎉)

  • Strict mode enabled by default

  • Live bindings (not copies!) that other modules can subscribe to

Think of it like a microservice it takes imports as inputs, does its work, and exposes exports as outputs.

🔍 Step-by-Step: What Happens When You import

Let’s take this example:

import { hello } from "./greetings.js";
console.log(hello("Anik"));

Here’s what really happens under the hood.

1️⃣ Parse & Build the Module Graph

The JS engine:

  • Parses your file into an AST

  • Collects all import/export statements statically

  • Builds a dependency graph for all modules

This is why imports must be top-level the engine needs to know every dependency before it starts running your code.

2️⃣ Resolve Specifiers

If you import "./greetings.js":

  • Relative imports → resolved against the current file URL

  • Bare imports (like "react") → Node does this dance:

    1. Look in node_modules

    2. Read package.json (exports or main)

    3. Fallback to index.js

If resolution fails, you’ll get Cannot find module before execution begins.

3️⃣ Fetch & Parse

The file is then fetched (disk in Node, network in browsers) and parsed again into an AST.

The engine stores it as a Module Record, which contains:

  • Its exported bindings

  • References to its imports

  • The executable code (but not run yet)

Now, the engine wires everything together.

Imported variables point directly to the module’s exports as live bindings:

// counter.js
export let count = 0;
export function increment() { count++; }
import { count, increment } from "./counter.js";

console.log(count); // 0
increment();
console.log(count); // 1 ✅ (auto-updated)

5️⃣ Execute

Finally, the module executes top-to-bottom.

  • Side effects happen here (logs, DB connects, etc.)

  • Execution happens only once

  • The result is stored in memory for later reuse

🧠 Module Caching: Your Hidden Performance Boost

Every runtime keeps a Module Map (URL → Module Record).

When you import the same file again:

  • The engine reuses the cached record

  • No re-execution happens (unless you clear cache manually)

This is why you don’t accidentally re-run initialization logic twice.

🌍 Global Module Map = Fresh per Runtime

Each browser tab or Node process has its own module map:

  • Refresh the page → clean slate

  • In Node, you can force a reload:

delete require.cache[require.resolve("./greetings.js")];
require("./greetings.js");

📜 Meet import.meta

Each module gets its own import.meta object think of it as the module’s passport:

console.log(import.meta.url);
// file:///absolute/path/to/greetings.js

Useful for resolving file-relative paths dynamically.

🏗 Organizing Your Codebase Like a Pro

Here’s a solid folder structure:

src/
  utils/
    format.js
    validate.js
  services/
    api.js
  index.js

You can even create a utils/index.js to re-export everything:

export * from "./format.js";
export * from "./validate.js";

Then just:

import { formatResult, validateInput } from "./utils/index.js";

✅ Cleaner imports
✅ Easier to refactor later

⚡ Static vs Dynamic Imports

Static Imports

✅ Loaded before execution
✅ Enable tree-shaking

import { sqrt } from "./math.js";

Dynamic Imports

✅ Loaded on demand
✅ Great for lazy-loading features

if (userWantsMath) {
  const math = await import("./math.js");
  console.log(math.sqrt(49));
}

Perfect for code-splitting in frameworks like Next.js, Vite, or Webpack.

🌀 Circular Imports: Handle With Care

Example:

// a.js
import { b } from "./b.js";
console.log("a sees b:", b);
export const a = "A";

// b.js
import { a } from "./a.js";
console.log("b sees a:", a);
export const b = "B";

Output:

b sees a: undefined
a sees b: B

Because linking happens first, execution later when b.js runs, a.js isn’t done yet.

Pro tip: Break the cycle by refactoring shared logic into a third module.

🏎 Engine Optimizations That Make This Fast

Modern JS engines (V8, SpiderMonkey) do some serious magic:

  • AST caching Parse once, reuse

  • Bytecode caching Skip parsing on reload

  • Speculative compilation Optimize hot functions early

  • Tree-shaking (via bundlers) removes unused code

If you’re curious, you can even see V8’s bytecode:

node --print-bytecode app.js

(Warning: It’s very nerdy, but very cool 😎)

🎯 Key Takeaways

  • Imports are resolved, linked, and cached before execution

  • Modules execute once per runtime

  • Use static imports for core code, dynamic imports for lazy-loaded features

  • Avoid circular imports they can cause weird partial initialization

  • Good folder structure makes scaling easier

Understanding this flow helps you write more predictable, faster code.
No more “why is my module running twice?” moments and no more mysterious undefined exports.

More from this blog

A

Anik Sikder - The Dev Loop

21 posts

Practical tips on building fast, scalable web apps using Python, JavaScript, and Web3. Learn how I turn ideas into reliable, production-ready digital products.