diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5b6781610b5129deb7a989758348270ba76925b..b7032949cc9f5cbb21bf87549169a79cbe21f5d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 * Support for SIMD based color space conversion - [#45](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/45)
+* Support for `window.requestAnimationFrame()` and game loop inversion of control - [#26](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/26)
 
 ### Changed
 
diff --git a/frontends/web/index.ts b/frontends/web/index.ts
index cd0defddda0d2b63119a0cd5146a59fb356b6876..ded99b96dd4f0da56c71f6c0405df328c887448f 100644
--- a/frontends/web/index.ts
+++ b/frontends/web/index.ts
@@ -58,5 +58,5 @@ const BACKGROUNDS = [
 
     // starts the emulator with the provided ROM URL, this is
     // going to run the main emulator (infinite) loop
-    await emulator.main({ romUrl: romUrl });
+    await emulator.start({ romUrl: romUrl });
 })();
diff --git a/frontends/web/package.json b/frontends/web/package.json
index cf92a9ff5c3fff4cab44afae1a6811bb79e671ab..4046de91593cd53c9a700a232d0a7ce542bf65ce 100644
--- a/frontends/web/package.json
+++ b/frontends/web/package.json
@@ -19,21 +19,21 @@
     "source": "index.ts",
     "devDependencies": {
         "@parcel/transformer-typescript-tsc": "^2.12.0",
-        "@types/react": "^18.2.61",
-        "@types/react-dom": "^18.2.19",
-        "@typescript-eslint/eslint-plugin": "^7.1.0",
-        "@typescript-eslint/parser": "^7.1.0",
+        "@types/react": "^18.3.3",
+        "@types/react-dom": "^18.3.0",
+        "@typescript-eslint/eslint-plugin": "^7.13.1",
+        "@typescript-eslint/parser": "^7.13.1",
         "buffer": "^6.0.3",
-        "emukit": "^0.9.8",
+        "emukit": "^0.10.0",
         "eslint": "^8.57.0",
         "jszip": "^3.10.1",
-        "nodemon": "^3.1.0",
+        "nodemon": "^3.1.3",
         "parcel": "^2.12.0",
-        "prettier": "^3.2.5",
+        "prettier": "^3.3.2",
         "process": "^0.11.10",
-        "react": "^18.2.0",
-        "react-dom": "^18.2.0",
-        "typescript": "^5.3.3",
+        "react": "^18.3.1",
+        "react-dom": "^18.3.1",
+        "typescript": "^5.4.5",
         "webgl-plot": "^0.7.1"
     }
 }
diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts
index 065f078c083924061f59f2bb701deb1fdbe54c07..8b6b624c7a43210926678a16d032faff65441568 100644
--- a/frontends/web/ts/gb.ts
+++ b/frontends/web/ts/gb.ts
@@ -8,7 +8,7 @@ import {
     Compiler,
     DebugPanel,
     Emulator,
-    EmulatorBase,
+    EmulatorLogic,
     Entry,
     Feature,
     Frequency,
@@ -40,7 +40,8 @@ import {
     Info,
     PadKey,
     SaveStateFormat,
-    StateManager
+    StateManager,
+    ClockFrame
 } from "../lib/boytacean";
 import info from "../package.json";
 
@@ -63,13 +64,6 @@ const LOGIC_HZ = 4194304;
  */
 const VISUAL_HZ = 59.7275;
 
-/**
- * The frequency of the pause polling update operation,
- * increasing this value will make resume from emulation
- * paused state fasted.
- */
-const IDLE_HZ = 10;
-
 const DISPLAY_WIDTH = 160;
 const DISPLAY_HEIGHT = 144;
 const DISPLAY_SCALE = 2;
@@ -81,13 +75,6 @@ const DISPLAY_SCALE = 2;
  */
 const STORE_RATE = 5;
 
-/**
- * The sample rate that is going to be used for FPS calculus,
- * meaning that every N seconds we will calculate the number
- * of frames rendered divided by the N seconds.
- */
-const FPS_SAMPLE_RATE = 3;
-
 const KEYS_NAME: Record<string, number> = {
     ArrowUp: PadKey.Up,
     ArrowDown: PadKey.Down,
@@ -116,7 +103,7 @@ export enum SerialDevice {
  * and "joins" all the elements together to bring input/output
  * of the associated machine.
  */
-export class GameboyEmulator extends EmulatorBase implements Emulator {
+export class GameboyEmulator extends EmulatorLogic implements Emulator {
     /**
      * The Game Boy engine (probably coming from WASM) that
      * is going to be used for the emulation.
@@ -138,17 +125,17 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
      */
     private autoMode = false;
 
-    private logicFrequency = LOGIC_HZ;
-    private visualFrequency = VISUAL_HZ;
-    private idleFrequency = IDLE_HZ;
+    protected logicFrequency = LOGIC_HZ;
+    protected visualFrequency = VISUAL_HZ;
 
-    private paused = false;
-    private nextTickTime = 0;
-    private fps = 0;
-    private frameStart: number = EmulatorBase.now();
-    private frameCount = 0;
     private paletteIndex = 0;
 
+    /**
+     * Number of pending CPU cycles from the previous tick.
+     * This is used to keep track of the overflow cycles.
+     */
+    private pending = 0;
+
     /**
      * The frequency at which the battery backed RAM is going
      * to be flushed to the `localStorage`.
@@ -169,6 +156,12 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
      */
     private extraSettings: Record<string, string> = {};
 
+    /**
+     * Current frame structure used in the clocking operations
+     * of the emulator, allowing deferred frame buffer retrieval.
+     */
+    private clockFrame: ClockFrame | null = null;
+
     constructor(extraSettings = {}) {
         super();
         this.extraSettings = extraSettings;
@@ -184,151 +177,47 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
     }
 
     /**
-     * Runs the initialization and main loop execution for
-     * the Game Boy emulator.
-     * The main execution of this function should be an
-     * infinite loop running machine `tick` operations.
+     * Runs a tick operation in the current emulator, this operation should
+     * be triggered at a regular interval to ensure that the emulator is
+     * properly updated.
      *
-     * @param options The set of options that are going to be
-     * used in he Game Boy emulator initialization.
+     * Not necessarily executed once per frame, but rather once per logic
+     * emulator unit.
+     *
+     * The tick operation is responsible for the following operations:
+     * - Clocks the system by the target number of cycles.
+     * - Triggers the frame event in case there's a frame to be processed.
+     * - Triggers the audio event, allowing the deferred retrieval of the audio buffer.
+     * - Flushes the RAM to the local storage in case the cartridge is battery backed.
      */
-    async main({ romUrl }: { romUrl?: string }) {
-        // boots the emulator subsystem with the initial
-        // ROM retrieved from a remote data source
-        await this.boot({ loadRom: true, romPath: romUrl ?? undefined });
-
-        // the counter that controls the overflowing cycles
-        // from tick to tick operation
-        let pending = 0;
-
-        // runs the sequence as an infinite loop, running
-        // the associated CPU cycles accordingly
-        while (true) {
-            // in case the machine is paused we must delay the execution
-            // a little bit until the paused state is recovered
-            if (this.paused) {
-                await new Promise((resolve) => {
-                    setTimeout(resolve, 1000 / this.idleFrequency);
-                });
-                continue;
-            }
-
-            // obtains the current time, this value is going
-            // to be used to compute the need for tick computation
-            let currentTime = EmulatorBase.now();
-
-            try {
-                pending = this.tick(
-                    currentTime,
-                    pending,
-                    Math.round(
-                        (this.logicFrequency *
-                            (this.gameBoy?.multiplier() ?? 1)) /
-                            this.visualFrequency
-                    )
-                );
-            } catch (err) {
-                // sets the default error message to be displayed
-                // to the user, this value may be overridden in case
-                // a better and more explicit message can be determined
-                let message = String(err);
-
-                // verifies if the current issue is a panic one
-                // and updates the message value if that's the case
-                const messageNormalized = (err as Error).message.toLowerCase();
-                const isPanic =
-                    messageNormalized.startsWith("unreachable") ||
-                    messageNormalized.startsWith("recursive use of an object");
-                if (isPanic) {
-                    message = "Unrecoverable error, restarting Game Boy";
-                }
-
-                // displays the error information to both the end-user
-                // and the developer (for diagnostics)
-                this.trigger("message", {
-                    text: message,
-                    error: true,
-                    timeout: 5000
-                });
-                console.error(err);
-
-                // pauses the machine, allowing the end-user to act
-                // on the error in a proper fashion
-                this.pause();
-
-                // if we're talking about a panic, proper action must be taken
-                // which in this case means restarting both the WASM sub
-                // system and the machine state (to be able to recover)
-                if (isPanic) {
-                    await wasm(true);
-                    await this.boot({ restore: false, reuse: false });
-
-                    this.trigger("error");
-                }
-            }
-
-            // calculates the amount of time until the next draw operation
-            // this is the amount of time that is going to be pending
-            currentTime = EmulatorBase.now();
-            const pendingTime = Math.max(this.nextTickTime - currentTime, 0);
-
-            // waits a little bit for the next frame to be draw,
-            // this should control the flow of render
-            await new Promise((resolve) => {
-                setTimeout(resolve, pendingTime);
-            });
-        }
-    }
-
-    tick(currentTime: number, pending: number, cycles = 70224) {
+    tick() {
         // in case the reference to the system is not set then
         // returns the control flow immediately (not possible to tick)
-        if (!this.gameBoy) return pending;
-
-        // in case the time to draw the next frame has not been
-        // reached the flush of the "tick" logic is skipped
-        if (currentTime < this.nextTickTime) return pending;
+        if (!this.gameBoy) return;
+
+        // calculates the number of cycles that are going to be
+        // processed in the current tick operation, this value is
+        // calculated using the logic and visual frequencies and
+        // the current Game Boy multiplier (DMG vs CGB)
+        const cycles = Math.round(
+            (this.logicFrequency * (this.gameBoy?.multiplier() ?? 1)) /
+                this.visualFrequency
+        );
 
         // initializes the counter of cycles with the pending number
         // of cycles coming from the previous tick
-        let counterCycles = pending;
-
-        let lastFrame = this.gameBoy.ppu_frame();
-
-        while (true) {
-            // limits the number of cycles to the provided
-            // cycle value passed as a parameter
-            if (counterCycles >= cycles) {
-                break;
-            }
-
-            // runs the Game Boy clock, this operations should
-            // include the advance of both the CPU and the PPU
-            const tickCycles = this.gameBoy.clock();
-            counterCycles += tickCycles;
-
-            // in case the frame is different from the previously
-            // rendered one then it's time to update the canvas
-            if (this.gameBoy.ppu_frame() !== lastFrame) {
-                // updates the reference to the last frame index
-                // to be used for comparison in the next tick
-                lastFrame = this.gameBoy.ppu_frame();
-
-                // triggers the frame event indicating that
-                // a new frame is now available for drawing
-                this.trigger("frame");
-            }
-
-            // in case the current cartridge is battery backed
-            // then we need to check if a RAM flush to local
-            // storage operation is required
-            if (this.cartridge && this.cartridge.has_battery()) {
-                this.flushCycles -= tickCycles;
-                if (this.flushCycles <= 0) {
-                    this.saveRam();
-                    this.flushCycles = this.logicFrequency * STORE_RATE;
-                }
-            }
+        let counterCycles = this.pending;
+
+        // clocks the system by the target number of cycles (deducted
+        // by the carryover cycles) and then in case there's at least
+        // a frame to be processed triggers the frame event, allowing
+        // the deferred retrieval of the frame buffer
+        this.clockFrame = this.gameBoy.clocks_frame_buffer(
+            cycles - counterCycles
+        );
+        counterCycles += Number(this.clockFrame.cycles);
+        if (this.clockFrame.frames > 0) {
+            this.trigger("frame");
         }
 
         // triggers the audio event, meaning that the audio should be
@@ -336,42 +225,25 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         // the audio buffer that is pending processing
         this.trigger("audio");
 
-        // increments the number of frames rendered in the current
-        // section, this value is going to be used to calculate FPS
-        this.frameCount += 1;
-
-        // in case the target number of frames for FPS control
-        // has been reached calculates the number of FPS and
-        // flushes the value to the screen
-        if (this.frameCount >= this.visualFrequency * FPS_SAMPLE_RATE) {
-            const currentTime = EmulatorBase.now();
-            const deltaTime = (currentTime - this.frameStart) / 1000;
-            const fps = Math.round(this.frameCount / deltaTime);
-            this.fps = fps;
-            this.frameCount = 0;
-            this.frameStart = currentTime;
+        // in case the current cartridge is battery backed
+        // then we need to check if a RAM flush to local
+        // storage operation is required
+        if (this.cartridge && this.cartridge.has_battery()) {
+            this.flushCycles -= counterCycles - this.pending;
+            if (this.flushCycles <= 0) {
+                this.saveRam();
+                this.flushCycles = this.logicFrequency * STORE_RATE;
+            }
         }
 
-        // calculates the number of ticks that have elapsed since the
-        // last draw operation, this is critical to be able to properly
-        // operate the clock of the CPU in frame drop situations, meaning
-        // a situation where the system resources are no able to emulate
-        // the system on time and frames must be skipped (ticks > 1)
-        if (this.nextTickTime === 0) this.nextTickTime = currentTime;
-        let ticks = Math.ceil(
-            (currentTime - this.nextTickTime) /
-                ((1 / this.visualFrequency) * 1000)
-        );
-        ticks = Math.max(ticks, 1);
-
-        // updates the next update time according to the number of ticks
-        // that have elapsed since the last operation, this way this value
-        // can better be used to control the game loop
-        this.nextTickTime += (1000 / this.visualFrequency) * ticks;
-
         // calculates the new number of pending (overflow) cycles
         // that are going to be added to the next iteration
-        return counterCycles - cycles;
+        this.pending = counterCycles - cycles;
+    }
+
+    async hardReset() {
+        await wasm(true);
+        await this.boot({ restore: false });
     }
 
     /**
@@ -655,7 +527,11 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
      * @returns The current pixel data for the emulator display.
      */
     get imageBuffer(): Uint8Array {
-        return this.gameBoy?.frame_buffer_eager() ?? new Uint8Array();
+        return (
+            this.clockFrame?.frame_buffer_eager() ??
+            this.gameBoy?.frame_buffer_eager() ??
+            new Uint8Array()
+        );
     }
 
     get audioSpecs(): AudioSpecs {
@@ -729,10 +605,6 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         return this.gameBoy.wasm_engine_wa() ?? null;
     }
 
-    get framerate(): number {
-        return this.fps;
-    }
-
     get registers(): Record<string, string | number> {
         const registers = this.gameBoy?.registers();
         if (!registers) return {};
@@ -805,7 +677,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
 
     async resume() {
         this.paused = false;
-        this.nextTickTime = EmulatorBase.now();
+        this.nextTickTime = EmulatorLogic.now();
     }
 
     async reset() {
@@ -899,11 +771,11 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         let cycles = 0;
         this.pause();
         try {
-            const initial = EmulatorBase.now();
+            const initial = EmulatorLogic.now();
             for (let i = 0; i < count; i++) {
                 cycles += this.gameBoy?.clock() ?? 0;
             }
-            const delta = (EmulatorBase.now() - initial) / 1000;
+            const delta = (EmulatorLogic.now() - initial) / 1000;
             const frequency_mhz = cycles / delta / 1000 / 1000;
             return {
                 delta: delta,
@@ -1092,6 +964,8 @@ console.image = (url: string, size = 80) => {
 };
 
 const wasm = async (setHook = true) => {
+    // waits for the WASM module to be (hard) re-loaded
+    // this should be an expensive operation
     await _wasm();
 
     // in case the set hook flag is set, then tries to
diff --git a/src/gb.rs b/src/gb.rs
index a6bc15ec9edb0bb8e3b0249a9d132cd514a031b4..c88000aa470d0ff1400e291e19fca9c8a08b30f3 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -308,6 +308,20 @@ pub trait AudioProvider {
     fn clear_audio_buffer(&mut self);
 }
 
+#[cfg_attr(feature = "wasm", wasm_bindgen)]
+pub struct ClockFrame {
+    pub cycles: u64,
+    pub frames: u16,
+    frame_buffer: Option<Vec<u8>>,
+}
+
+#[cfg_attr(feature = "wasm", wasm_bindgen)]
+impl ClockFrame {
+    pub fn frame_buffer_eager(&mut self) -> Option<Vec<u8>> {
+        self.frame_buffer.take()
+    }
+}
+
 /// Top level structure that abstracts the usage of the
 /// Game Boy system under the Boytacean emulator.
 /// Should serve as the main entry-point API.
@@ -491,6 +505,54 @@ impl GameBoy {
         cycles
     }
 
+    /// Clocks the emulator until the limit of cycles that has been
+    /// provided and returns the amount of cycles that have been
+    /// clocked.
+    pub fn clocks_cycles(&mut self, limit: usize) -> u64 {
+        let mut cycles = 0_u64;
+        loop {
+            cycles += self.clock() as u64;
+            if cycles >= limit as u64 {
+                break;
+            }
+        }
+        cycles
+    }
+
+    /// Clocks the emulator until the limit of cycles that has been
+    /// provided and returns the amount of cycles that have been
+    /// clocked together with the frame buffer of the PPU.
+    ///
+    /// Allows a caller to clock the emulator and at the same time
+    /// retrieve the frame buffer of the PPU at the proper timing
+    /// (on V-Blank).
+    ///
+    /// This method allows for complex foreign call optimizations
+    /// by preventing the need to call the emulator clock multiple
+    /// times to obtain the right frame buffer retrieval timing.
+    pub fn clocks_frame_buffer(&mut self, limit: usize) -> ClockFrame {
+        let mut cycles = 0_u64;
+        let mut frames = 0_u16;
+        let mut frame_buffer: Option<Vec<u8>> = None;
+        let mut last_frame = self.ppu_frame();
+        loop {
+            cycles += self.clock() as u64;
+            if self.ppu_frame() != last_frame {
+                frame_buffer = Some(self.frame_buffer().to_vec());
+                last_frame = self.ppu_frame();
+                frames += 1;
+            }
+            if cycles >= limit as u64 {
+                break;
+            }
+        }
+        ClockFrame {
+            cycles,
+            frames,
+            frame_buffer,
+        }
+    }
+
     pub fn next_frame(&mut self) -> u32 {
         let mut cycles = 0u32;
         let current_frame = self.ppu_frame();