Optimization Tips
PythonExtra is fast enough for real-time games — if you’re careful.
But if your app starts lagging, there are three usual culprits:
- Drawing too much
- Inefficient algorithms
- Memory allocation & GC abuse
This page shows how to spot those issues and fix them, with concrete examples.
1. Avoid Unnecessary Overdraw
You can easily get 20+ FPS with full-screen redraws — as long as you’re not redrawing what doesn’t need to change.
❌ Bad: Redrawing things you’re about to overwrite
# Clear the screen, then draw a full tileset right over it
drect(0, 0, DWIDTH-1, DHEIGHT-1, C_WHITE)
for y in range(0, DHEIGHT, 16):
for x in range(0, DWIDTH, 16):
drect(x, y, x+15, y+15, C_GRAY) # Overwrites previous draw
You’re wasting time filling pixels with white only to overwrite them immediately.
âś… Better: Just draw the tiles
for y in range(0, DHEIGHT, 16):
for x in range(0, DWIDTH, 16):
drect(x, y, x+15, y+15, C_GRAY)
Unless you need to clear parts of the screen (like UI or old objects), don’t redraw them.
2. Avoid Rebuilding Data Structures
Constantly creating new lists/tuples/objects every frame leads to spikes in garbage collection, especially if they’re small.
❌ Bad: Reallocating every frame
# Builds a new list of coords every time
pixels = [(x * 10, y * 10) for x, y in snake]
This creates N tuples per frame, and GC hates it.
âś… Better: Use preallocated buffers (but still not perfect)
# Still makes tuples, but reuses the list
pixels = [(0, 0)] * 100
for i, (x, y) in enumerate(snake):
pixels[i] = (x * 10, y * 10)
âś…âś… Even better: Use flat bytes
or bytearray
# 100 pairs = 200 bytes
buf = bytearray(200)
for i, (x, y) in enumerate(snake):
buf[i*2] = x
buf[i*2+1] = y
bytes
/bytearray
are 4x cheaper per value than tuples- Especially useful when values fit in 0–255 range (like grid coordinates or sprite IDs)
- You get CPU cache friendliness and less GC overhead
3. Avoid Allocating Thousands of Small Values
If you’re building a huge array of tiny tuples, you’re feeding the garbage collector a buffet of objects it has to track and clean.
❌ Bad: 1,000 tiny tuples
positions = [(x, x) for x in range(1000)]
That’s 1,000 allocations — and it gets worse if you do this in a loop.
âś… Better: Use a flat array or shared buffer
positions = bytearray(2000)
for i in range(1000):
positions[i*2] = i % 256
positions[i*2+1] = i // 4
- Uses one object instead of 1,000
- Fits better in memory
- No GC required
This trick is essential for tilemaps, image buffers, path grids, etc.
4. Avoid Doing Heavy Work Every Frame
❌ Bad: Sorting a list every tick
def find_closest(entities):
return sorted(entities, key=distance)[0]
# In the main loop
closest = find_closest(enemies)
Even for 30–50 items, this adds up fast.
âś… Better: Cache or stagger expensive work
if frame % 10 == 0:
closest = min(enemies, key=distance)
Spread expensive calculations across frames, or only update when data changes.
TL;DR – What to Watch
Problem | Avoid this | Do this instead |
---|---|---|
Overdrawing | Clearing then overdrawing | Only draw what’s visible/changed |
GC abuse | Allocating lists/tuples constantly | Preallocate or use bytearray |
Algorithm bottlenecks | Sorting/pathfinding every frame | Update on timer or when needed only |
Extra Notes
drect()
includes both x2/y2 — so full-screen isdrect(0, 0, DWIDTH-1, DHEIGHT-1, color)
dupdate()
is mandatory to display changes. Call it once per frame.- For input-heavy loops,
clearevents()
andcleareventflips()
are your friends. - Partial redraws (only updating specific screen regions) can improve performance but are tricky. Unless you’re drawing fewer than 3 things per frame, full redraw is simpler and usually fast enough.
Need help diagnosing your frame drops?
Ask the 👨‍💻 PythonExtra ChatGPT — just paste your loop or logic!