Quick answer: Pygame blitting is slow because the surfaces have not been converted to the display’s pixel format. Call .convert() on opaque images and .convert_alpha() on images with transparency immediately after loading. This eliminates per-pixel format conversion during blit and typically gives a 3-6x speed improvement.
Here is how to fix Pygame surface convert_alpha slow performance. Your game runs at 15 FPS on a machine that should easily handle 2D sprite rendering. You profile with pygame.time.Clock and discover that screen.blit() calls are consuming almost all of your frame budget. You are not doing anything unusual — just blitting a background, a tilemap, and a handful of sprites. The problem is not what you are blitting, but how the surfaces are stored in memory. Unconverted surfaces force Pygame to translate pixel formats on every single blit, turning a fast memory copy into an expensive per-pixel computation.
The Symptom
Your Pygame game runs much slower than expected. The frame rate drops as you add more sprites, but even a modest number of blits (20-50 per frame) causes noticeable slowdown. Profiling reveals that the majority of time is spent inside blit() calls, not in your game logic, physics, or input handling.
You try reducing the number of sprites, using smaller images, or blitting less frequently, and performance improves proportionally. This confirms that the cost is per-blit and scales with the number and size of surfaces. You are not leaking surfaces or blitting the entire screen unnecessarily — the blits themselves are just expensive.
The same code runs fast on some machines and slow on others, or runs fast with some image formats (BMP) and slow with others (PNG with alpha). This is the clue: the speed difference comes from whether the surface’s internal pixel format happens to match the display’s format. When they match, blitting is a fast memory copy. When they do not match, every pixel must be converted individually.
What Causes This
Unconverted surfaces. When you call pygame.image.load("sprite.png"), Pygame creates a surface in whatever pixel format the PNG file uses — typically 32-bit RGBA. Your display surface, created by pygame.display.set_mode(), uses whatever format the OS and hardware prefer — often 24-bit RGB or 32-bit RGBX (no alpha channel). These formats are different. When you blit an RGBA surface onto an RGB display, Pygame must convert every pixel from RGBA to RGB, which involves reading the source pixel, extracting channels, handling alpha, and writing the result. This per-pixel conversion happens every frame, for every blit.
Using convert_alpha() when convert() would suffice. convert_alpha() preserves per-pixel alpha transparency and converts the surface to a format optimized for alpha blending. But alpha blending itself is more expensive than opaque blitting. For every pixel, the renderer must read the destination pixel, read the source pixel, multiply by alpha, add the results, and write back. For opaque surfaces (backgrounds, solid tiles), this alpha blending is wasted work. Using convert() instead tells Pygame the surface is fully opaque, enabling a fast direct copy without reading the destination.
Creating surfaces without converting them. Surfaces created with pygame.Surface((w, h)) or returned by font.render() are also in their own pixel format that may not match the display. If you create a surface, draw on it, and then blit it every frame, the same format-conversion overhead applies. This is easy to miss because pygame.Surface() does not involve loading an image file.
The Fix
Step 1: Convert every surface immediately after creation. Make it a rule: every pygame.image.load() call must be followed by .convert() or .convert_alpha(). No exceptions.
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
# SLOW: unconverted surface, format mismatch every blit
# bg_slow = pygame.image.load("background.png")
# FAST: opaque image, no transparency needed
bg = pygame.image.load("background.png").convert()
# FAST: sprite with transparency (per-pixel alpha)
player = pygame.image.load("player.png").convert_alpha()
# FAST: dynamically created surface, converted
overlay = pygame.Surface((200, 50), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 128))
overlay = overlay.convert_alpha()
# FAST: text surface, converted
font = pygame.font.SysFont("arial", 24)
text = font.render("Score: 0", True, (255, 255, 255))
text = text.convert_alpha()
The convert() and convert_alpha() calls return a new surface in the display’s native format. The original surface is discarded. This one-time conversion cost at load time saves repeated conversion costs every frame at blit time.
Step 2: Choose the right conversion method. The rule is simple: if the image has transparency, use convert_alpha(). If it does not, use convert(). Opaque blits are significantly faster.
def load_image(path, has_alpha=False):
"""Load and convert an image for optimal blit speed."""
img = pygame.image.load(path)
if has_alpha:
return img.convert_alpha()
return img.convert()
# Usage
background = load_image("bg.png") # opaque
tileset = load_image("tiles.png") # opaque
player_sprite = load_image("hero.png", True) # has alpha
particle = load_image("spark.png", True) # has alpha
A common mistake is to use convert_alpha() for everything “just to be safe.” This works correctly but leaves performance on the table. Backgrounds and tilesets that cover the entire screen do not need alpha blending — every pixel is overwritten completely. Using convert() for these surfaces can double the blit speed for those specific surfaces.
Measuring the Difference
The performance difference is dramatic and easy to measure. Create a simple benchmark that blits a surface 1,000 times per frame and compare the FPS with and without conversion. On a typical machine, you will see results like this:
- Unconverted PNG (RGBA): ~15 FPS
- convert_alpha(): ~55 FPS
- convert() (opaque): ~120 FPS
The exact numbers vary by hardware, surface size, and display format, but the relative improvements are consistent. convert() is roughly 2x faster than convert_alpha(), and convert_alpha() is roughly 3-4x faster than no conversion. For a game with 50 sprites per frame, this is the difference between a smooth 60 FPS and an unplayable 10 FPS.
Per-Pixel Alpha vs. Colorkey Transparency
If your sprites use a solid background color (like magenta) as a transparency marker rather than an actual alpha channel, you can use convert() with set_colorkey() instead of convert_alpha(). Colorkey transparency is faster than per-pixel alpha because the renderer only checks one condition per pixel (does this color match the key?) rather than performing a full alpha blend.
However, colorkey transparency produces hard edges with no anti-aliasing. Per-pixel alpha from convert_alpha() supports smooth edges and semi-transparent pixels. For retro pixel art games, colorkey is often the better choice both visually and performance-wise. For games with smooth modern sprites, per-pixel alpha is necessary.
“Every unconverted surface in your game is a hidden tax on every frame. The fix takes one line of code per image and the speedup is immediate. There is no reason to skip it.”
Related Issues
If your game is still slow after converting all surfaces, see Display Update Redrawing Full Screen Every Frame for dirty rect optimization techniques. If transparent surfaces appear with a black background instead of transparency, check Transparent Surface Showing Black for SRCALPHA flag and per-surface alpha settings.
Call .convert() or .convert_alpha() on every surface right after loading — never blit an unconverted surface.