Light Entity Class: Memory Optimizations
ESPHome 2025.11.0 introduces memory optimizations to the Light entity class that reduce both heap and flash usage. These changes affect external components implementing custom light devices and effects.
Background
Section titled “Background”Motivation
Section titled “Motivation”Light components are among the most commonly used in ESPHome, and their memory footprint directly impacts available heap for runtime operations. On ESP8266 devices with only ~40KB of usable heap, every optimization matters:
- Effect names were stored as
std::stringobjects on the heap, consuming allocation overhead (typically 24+ bytes) plus the string data for each effect - Color mode sets used
std::set<ColorMode>with red-black tree structure, consuming ~80 bytes of base overhead plus additional overhead for each member (2-4 modes typical) - Light components often have multiple effects (5-10 common) and support several color modes, making this overhead multiply across each light entity
Moving effect names to flash and color modes to a 2-byte bitmask frees heap memory for runtime operations like network buffers, API operations, and component state. The bitmask also provides much faster O(1) lookups compared to the red-black tree’s more expensive O(log n) operations.
Changes
Section titled “Changes”Two optimization PRs improved Light entity memory usage:
PR #11487: Store Effect Names in Flash
Changes effect name storage from std::string (heap) to const char* (flash). Saves string allocation overhead plus string data for each effect.
PR #11348: Use Bitmask for Color Modes
Changes color mode storage from std::set<ColorMode> (red-black tree on heap) to ColorModeMask (uint16_t bitmask, 2 bytes). Saves ~80 bytes of base std::set overhead plus per-member overhead. Provides much faster O(1) lookups.
What’s Changing
Section titled “What’s Changing”For ESPHome 2025.11.0 and Later
Section titled “For ESPHome 2025.11.0 and Later”Effect Name Storage Changes (Breaking - PR #11487):
// OLD - std::string in heapclass MyEffect : public LightEffect { public: explicit MyEffect(const std::string &name) : LightEffect(name) {}};
// NEW - const char* in flashclass MyEffect : public LightEffect { public: explicit MyEffect(const char *name) : LightEffect(name) {}};Color Mode Storage Changes (Mostly Backward Compatible - PR #11348):
// OLD - std::set in heap#include <set>std::set<ColorMode> modes = {ColorMode::RGB, ColorMode::WHITE};traits.set_supported_color_modes(modes);bool supports = traits.get_supported_color_modes().count(ColorMode::RGB);
// NEW - ColorModeMask bitmask (but old syntax still works!)#include "color_mode.h"traits.set_supported_color_modes({ColorMode::RGB, ColorMode::WHITE});bool supports = traits.get_supported_color_modes().count(ColorMode::RGB); // .count() still worksWho This Affects
Section titled “Who This Affects”Effect Name Changes (PR #11487) - External components likely affected:
- Any custom
LightEffectsubclasses need constructor updates fromstd::stringtoconst char* - Any code that calls
get_name()on effects
Color Mode Changes (PR #11348) - External components mostly backward compatible:
ColorModeMaskprovides backward compatibility withstd::setAPI (has.count(),.insert(),.erase(),.size(),.empty())- Most components already pass initializer lists like
{ColorMode::RGB, ColorMode::WHITE}which continue to work - No core components needed changes, so external components are unlikely to need changes
- Breaking edge case: Explicitly passing
std::set<ColorMode>toset_supported_color_modes()will fail to compile (but this pattern was not used in any core components)
Standard YAML configurations work without code changes.
Migration Guide for External Components
Section titled “Migration Guide for External Components”Part 1: Effect Name Changes
Section titled “Part 1: Effect Name Changes”1. Update Effect Constructors (Required Now)
Section titled “1. Update Effect Constructors (Required Now)”// OLD#include <string>
class MyEffect : public LightEffect { public: explicit MyEffect(const std::string &name) : LightEffect(name) {}};
// NEW - use const char*class MyEffect : public LightEffect { public: explicit MyEffect(const char *name) : LightEffect(name) {}};2. Update Effect get_name() Usage (Required Now)
Section titled “2. Update Effect get_name() Usage (Required Now)”// OLD - returns const std::string&const std::string &name = effect->get_name();if (name == "My Effect") { }
// NEW - returns const char*const char *name = effect->get_name();if (strcmp(name, "My Effect") == 0) { }3. Remove Unnecessary String Includes
Section titled “3. Remove Unnecessary String Includes”// Remove if only used for effect names:#include <string>Part 2: Color Mode Changes
Section titled “Part 2: Color Mode Changes”1. Update Container Types (Required Now)
Section titled “1. Update Container Types (Required Now)”// OLD#include <set>std::set<ColorMode> modes_;
// NEW#include "esphome/components/light/color_mode.h"ColorModeMask modes_;2. Update Setter Calls (Only If You Explicitly Created std::set Variables)
Section titled “2. Update Setter Calls (Only If You Explicitly Created std::set Variables)”Note: Most components already pass initializer lists directly and don’t need changes. This only affects code that explicitly creates std::set variables (which no core components did).
// OLD - explicitly creating std::set (uncommon, will fail to compile)std::set<ColorMode> modes = {ColorMode::RGB, ColorMode::WHITE};traits.set_supported_color_modes(modes); // No longer has std::set overload
// NEW - initializer list (most components already did this)traits.set_supported_color_modes({ColorMode::RGB, ColorMode::WHITE});
// OR - change std::set to ColorModeMaskColorModeMask modes({ColorMode::RGB, ColorMode::WHITE});traits.set_supported_color_modes(modes); // Works3. Lookups Work Without Changes
Section titled “3. Lookups Work Without Changes”Note: ColorModeMask provides .count() for backward compatibility, so no changes are required.
// Works exactly as before - no changes neededif (traits.get_supported_color_modes().count(ColorMode::RGB)) { // mode is supported}4. Update Capability Checks (Optional - But Recommended)
Section titled “4. Update Capability Checks (Optional - But Recommended)”Note: The old manual loop still works due to iterator compatibility, but using supports_color_capability() is cleaner.
// OLD - manual loop (still works)bool has_brightness = false;for (auto mode : traits.get_supported_color_modes()) { if (mode & ColorCapability::BRIGHTNESS) { has_brightness = true; break; }}
// NEW - recommended approachbool has_brightness = traits.supports_color_capability(ColorCapability::BRIGHTNESS);Complete Migration Examples
Section titled “Complete Migration Examples”Example 1: Custom Effect
Section titled “Example 1: Custom Effect”Before:
#include <string>
class RainbowEffect : public LightEffect { public: explicit RainbowEffect(const std::string &name) : LightEffect(name) {}
void apply() override { const std::string &name = this->get_name(); ESP_LOGD("effect", "Applying %s", name.c_str()); // Effect implementation }};After:
class RainbowEffect : public LightEffect { public: explicit RainbowEffect(const char *name) : LightEffect(name) {}
void apply() override { const char *name = this->get_name(); ESP_LOGD("effect", "Applying %s", name); // Effect implementation }};Example 2: Custom Light Component
Section titled “Example 2: Custom Light Component”Before:
#include <set>
class MyLight : public LightOutput { public: LightTraits get_traits() override { auto traits = LightTraits(); std::set<ColorMode> modes = {ColorMode::RGB, ColorMode::WHITE}; traits.set_supported_color_modes(modes); return traits; }
void check_mode_support(ColorMode mode) { auto modes = this->get_traits().get_supported_color_modes(); if (modes.count(mode)) { // Mode is supported } }};After:
#include "esphome/components/light/color_mode.h"
class MyLight : public LightOutput { public: LightTraits get_traits() override { auto traits = LightTraits(); traits.set_supported_color_modes({ColorMode::RGB, ColorMode::WHITE}); return traits; }
void check_mode_support(ColorMode mode) { auto modes = this->get_traits().get_supported_color_modes(); if (modes.count(mode)) { // Mode is supported (no changes needed - .count() still works!) } }};Lifetime Safety for Effect Names
Section titled “Lifetime Safety for Effect Names”All const char* pointers must point to memory that lives for the effect’s lifetime:
Safe patterns:
// 1. String literals (preferred) - stored in flashauto effect = new MyEffect("Rainbow");
// 2. Static constantsstatic const char *const EFFECT_NAME = "Rainbow";auto effect = new MyEffect(EFFECT_NAME);
// 3. C arraysstatic constexpr const char *const EFFECT_NAMES[] = {"Rainbow", "Pulse"};auto effect = new MyEffect(EFFECT_NAMES[0]);Unsafe patterns (DO NOT USE):
// WRONG - temporary stringstd::string temp = "Rainbow";auto effect = new MyEffect(temp.c_str()); // Dangling pointer!
// WRONG - local arrayconst char name[] = "Rainbow";auto effect = new MyEffect(name); // Array destroyed!ColorModeMask Details
Section titled “ColorModeMask Details”Performance Benefits
Section titled “Performance Benefits”ColorModeMask uses a 2-byte (uint16_t) bitmask instead of a red-black tree:
- Memory: 2 bytes vs ~80 bytes base overhead + per-member overhead
- Lookup speed: O(1) single bitwise AND operation vs O(log n) tree traversal
- Cache efficiency: Fits in CPU cache vs pointer chasing through tree nodes
For typical light use cases (2-4 color modes, frequent lookups), this provides significant performance and memory improvements.
Iterator Support
Section titled “Iterator Support”ColorModeMask provides iterator support for compatibility with API encoding:
ColorModeMask modes({ColorMode::RGB, ColorMode::WHITE});
// Range-based for loopfor (auto mode : modes) { ESP_LOGD("light", "Supported mode: %d", static_cast<int>(mode));}
// Manual iterationfor (auto it = modes.begin(); it != modes.end(); ++it) { ColorMode mode = *it;}Size and Emptiness Checks
Section titled “Size and Emptiness Checks”ColorModeMask modes({ColorMode::RGB, ColorMode::WHITE});
size_t count = modes.size(); // Returns 2bool is_empty = modes.empty(); // Returns falseRaw Bitmask Access
Section titled “Raw Bitmask Access”For advanced use cases (e.g., API encoding):
ColorModeMask modes({ColorMode::RGB, ColorMode::WHITE});uint16_t raw_mask = modes.get_mask(); // Get raw bitmask valueTimeline
Section titled “Timeline”- ESPHome 2025.11.0 (November 2025):
- Effect name changes are active (breaking for custom effects)
- Color mode changes are active (backward compatible - no changes required)
Finding Code That Needs Updates
Section titled “Finding Code That Needs Updates”Search your external component code for these patterns:
# Find std::string usage for effect namesgrep -r 'LightEffect.*std::string' --include='*.cpp' --include='*.h'
# Find std::set usage for color modesgrep -r 'std::set<ColorMode>' --include='*.cpp' --include='*.h'
# Find .count() method calls on color modesgrep -r 'get_supported_color_modes.*count' --include='*.cpp' --include='*.h'
# Find set_supported_color_modes callsgrep -r 'set_supported_color_modes' --include='*.cpp' --include='*.h'Questions?
Section titled “Questions?”If you have questions about these changes or need help migrating your external component, please ask in the ESPHome Discord or open a discussion on GitHub.
Related Documentation
Section titled “Related Documentation”Copyright © 2026 ESPHome - A project from the Open Home Foundation
Comments
Feel free to leave a comment here to discuss this post with others. You can ask questions, share your experience, or suggest improvements. If you have a question about a specific feature or issue, please consider using the ESPHome Discord. Stick to English and follow ESPHome's code of conduct. These comments exist on a discussion on GitHub, so you can also comment there directly if you prefer.