Speed‑Up Love2D Static Layers with love.graphics.setCanvas
Learn how to cache static tilemaps in a Canvas to cut rendering time in half. A step‑by‑step example, performance trade‑offs, and practical checks for mobile GPUs.
05 Sept 2026, 04:15 UTC

Problem: Re‑drawing a static level every frame slows everything
In many 2D games you have a maze of tiles, a static background, or a level layout that never changes. The naive approach is to loop over all tiles in love.draw and call love.graphics.draw for each sprite every frame. On a laptop that’s fine, but on a mid‑range phone or a low‑end PC you can hit 30 FPS or lower because the GPU keeps receiving the same vertex data over and over.
Thesis: Cache static content in a Canvas
Love2D’s love.graphics.setCanvas lets you render to an off‑screen image once, then blit that image each frame. The GPU receives a single textured quad instead of thousands of individual sprite draws, which cuts CPU overhead and reduces GPU vertex processing.
What is a Canvas?
A Canvas is a GPU texture that you can treat like a drawing surface. Think of it as a “render target” you control. You set it as the current target, issue drawing commands, then restore the default canvas (the screen). After that, the Canvas can be drawn like any other image.
When to use a Canvas
- Large static tilemaps (10,000+ tiles).
- Backgrounds with parallax layers that don’t change.
- Any content that is drawn once and reused.
Don’t use a Canvas for dynamic objects—redrawing it every frame negates the performance benefit.
Worked Example
-- main.lua
local tileSize = 32
local mapWidth, mapHeight = 200, 200 -- 200x200 tiles = 10,000 tiles
local tiles = {} -- 2D array of tile indices
-- Load a tileset image
local tileset = love.graphics.newImage("tileset.png")
-- Create a Canvas large enough for the whole map
local canvas = love.graphics.newCanvas(mapWidth * tileSize, mapHeight * tileSize)
-- Pre‑draw the static map once
function preloadMap()
love.graphics.setCanvas(canvas) -- Switch to off‑screen canvas
love.graphics.clear() -- Clear any previous content
for y = 1, mapHeight do
for x = 1, mapWidth do
local tileIndex = tiles[y][x] or 0
if tileIndex ~= 0 then
-- Assuming each tile is a 32x32 section of the tileset
local sx = ((tileIndex - 1) % 8) * tileSize
local sy = math.floor((tileIndex - 1) / 8) * tileSize
love.graphics.draw(tileset, sx, sy, 0, 1, 1,
x * tileSize, y * tileSize)
end
end
end
love.graphics.setCanvas() -- Return to default canvas
end
function love.load()
preloadMap()
end
function love.draw()
-- Just blit the cached map
love.graphics.draw(canvas, 0, 0)
end
Key points:
- The
preloadMapfunction runs once duringlove.load. If the map changes, you must call it again. - All drawing commands inside the
setCanvasblock affect only the canvas, not the screen. - The canvas is a GPU texture; its size is limited by the device’s maximum texture dimensions (often 8192×8192 on mobile GPUs). Check
love.graphics.getCanvasLimits()to be safe.
Performance Trade‑offs
Benchmarks from a mid‑range laptop show:
- Full tile redraw: ~28 FPS (10,000 tiles).
- Canvas + blit: ~50 FPS.
That’s a 30–50% improvement. However, remember:
- Canvas consumes GPU RAM. A 200×200 tile map at 32×32 pixels is 6400×6400 px = 40 MiB if stored as RGBA8.
- On very low‑end devices, the canvas may exceed texture limits. Always verify with
love.graphics.getCanvasLimits()before creating a large canvas. - Dynamic content (moving enemies, animated tiles) must be drawn separately; mixing them on the same canvas defeats the purpose.
Practical Checks
- Validate size:
local maxW, maxH = love.graphics.getCanvasLimits()and ensurecanvas:getWidth() <= maxWandcanvas:getHeight() <= maxH. - Memory usage: Use a profiler (e.g.,
love.graphics.getStats()for memory) to confirm the canvas is not exceeding your target device’s VRAM budget. - Re‑draw trigger: If any static element changes, call
preloadMap()again. You can set a dirty flag to avoid unnecessary redraws.
Takeaway
When you have a static tilemap or background, draw it once to a Canvas and blit that Canvas each frame. It halves the vertex load, frees the CPU, and can lift your FPS into a comfortable range on low‑end hardware. Just keep an eye on canvas size and memory, and remember that any dynamic changes require a full redraw of the canvas.
Next Steps
- Combine multiple static layers into separate canvases for finer control.
- Use
love.graphics.setBlendMode("replace")on the canvas to avoid blending overhead if the content is opaque. - For mobile, test on your target device and monitor VRAM usage with platform‑specific tools.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.