diff --git a/components/st3m/st3m_gfx.c b/components/st3m/st3m_gfx.c
index a2facf3bf34f26a47e16d8147b5c7cd81f7803bd..a88e58e756608f2acd8820433ac33f0791312432 100644
--- a/components/st3m/st3m_gfx.c
+++ b/components/st3m/st3m_gfx.c
@@ -81,7 +81,8 @@ static void st3m_gfx_crtc_task(void *_arg) {
         st3m_counter_timer_sample(&blit_read_time, end - start);
 
         start = esp_timer_get_time();
-        flow3r_bsp_display_send_fb(framebuffer_descs[descno].buffer);
+        if (!framebuffer_descs[descno].empty)
+            flow3r_bsp_display_send_fb(framebuffer_descs[descno].buffer);
         end = esp_timer_get_time();
         st3m_counter_timer_sample(&blit_work_time, end - start);
 
@@ -131,7 +132,19 @@ static void st3m_gfx_rast_task(void *_arg) {
 
         // Render drawctx into fbctx.
         start = esp_timer_get_time();
-        ctx_render_ctx(draw->ctx, fb->ctx);
+        int count = 0;
+        const CtxEntry *drawlist = ctx_get_drawlist(draw->ctx, &count);
+
+        // XXX maybe we should just rely on count < 4 meaning empty,
+        // this is after-all a time critical path
+        if ((count == 4 && (drawlist[0].code == CTX_SAVE) &&
+             (drawlist[1].code == CTX_SAVE)) ||
+            count <= 2) {
+            fb->empty = 1;
+        } else {
+            fb->empty = 0;
+            ctx_render_ctx(draw->ctx, fb->ctx);
+        }
         ctx_drawlist_clear(draw->ctx);
         end = esp_timer_get_time();
         st3m_counter_timer_sample(&rast_work_time, end - start);
diff --git a/components/st3m/st3m_gfx.h b/components/st3m/st3m_gfx.h
index e5a2d94b6aab70834ec9ec0cd6923461404ad27b..571779459eb53543dd15d4dc19d3b0916491e154 100644
--- a/components/st3m/st3m_gfx.h
+++ b/components/st3m/st3m_gfx.h
@@ -18,6 +18,8 @@
 typedef struct {
     // The numeric ID of this descriptor.
     int num;
+    // set when the drawlist was empty
+    int empty;
     // SPIRAM buffer.
     uint16_t buffer[240 * 240];
     Ctx *ctx;
@@ -50,7 +52,7 @@ uint8_t st3m_gfx_drawctx_pipe_full(void);
 // Flush any in-flight pipelined work, resetting the free ctx/framebuffer queues
 // to their initial state. This should be called if there has been any drawlist
 // ctx dropped (ie. drawctx_free_get was called but then drawctx_pipe_put
-// wasn't, for exaple if Micropython restarted).
+// wasn't, for example if Micropython restarted).
 //
 // This causes a graphical disturbance and shouldn't be called during normal
 // operation.
@@ -74,4 +76,4 @@ void st3m_gfx_splash(const char *text);
 
 // Draw the flow3r multi-coloured logo at coordinates x,y and with given
 // dimension (approx. bounding box size).
-void st3m_gfx_flow3r_logo(Ctx *ctx, float x, float y, float dim);
\ No newline at end of file
+void st3m_gfx_flow3r_logo(Ctx *ctx, float x, float y, float dim);
diff --git a/python_payload/st3m/reactor.py b/python_payload/st3m/reactor.py
index b24384d8dd7778ed5564ec9ecfeae739dc4f5e4d..42ed2d164ec8c57b775bdad7fc90f0dddf23a595 100644
--- a/python_payload/st3m/reactor.py
+++ b/python_payload/st3m/reactor.py
@@ -40,7 +40,9 @@ class Responder(ABCBase):
         coordinates are +/- 120 in both X and Y (positive numbers towards up and
         right), with 0,0 being the middle of the screen.
 
-        The Reactor will then rasterize and blit the result.
+        The Reactor will then rasterize and blit the result. If no ctx drawing
+        commands are issued, it is interpreted as a wish to keep the display
+        contents as-is, and cpu time is not spent on rasterization and blitting.
 
         The code must not sleep or block during this callback, as that will
         impact the system tickrate and framerate.