Beware of Color.value's performance

So, last year, Flutter switched from simple representation of color using a 32-bit int (e.g. 0xFF006611) to something much more involved. This was done in order to support wide gamut colors, AFAIK.

Since then, Color is no longer a tiny box over an integer, but it’s 4 doubles and a color space. Still, it’s of course possible to convert the color back to a 32-bit integer. Implicitly, this is done when you access Color.value, and explicitly, you can call the toARGB32() method.

Back in December 2024, folks (including me) asked if it would be possible to have some kind of “8bit color” back, but understandably, this was not going to fly.

Anyway, if you’re only doing a few Color operations per frame, it’s fine, of course. But I have a case in my game where I construct a color, then do operations on it (Color has a nice API for things like making a color brighter), and then feed it to a Canvas.drawVertices() call thousands of times per frame.

I therefore knew the change will have some performance implication for me if I just use Color.value, but I didn’t measure it. The game performs okay.

Today, I measured, and found out that Color.value took something like 10% of my game’s CPU time. That’s… significant.

By removing Color and just going with a simple integer, plus some basic manipulation of it, I was able to remove this overhead altogether.

I know my case is niche but I thought I’d share nevertheless.

P.S.: Color.value is deprecated, and has been for months (you should use the more explicit Color.toARBG32(), which is more explicit about doing some work). So it’s unlikely people who use Color.value are completely unaware of this issue. But maybe, like me, they let themselves sleep on it.

6 Likes

That doesn’t make sense. This is Color source code:

@Deprecated('Use component accessors like .r or .g, or toARGB32 for an explicit conversion')
int get value => toARGB32();

int toARGB32() {
  return _floatToInt8(a) << 24 |
    _floatToInt8(r) << 16 |
    _floatToInt8(g) << 8 |
    _floatToInt8(b) << 0;
}

static int _floatToInt8(double x) {
  return (x * 255.0).round() & 0xff;
}

Or I misunderstood you or you said toARGB32() is faster than value (while in fact they are the same).

Also, the math behind the double to int is not that heavy. Maybe you found something more problematic with Dart math operations?

(BTW, why in the hell the redundant << 0 and & 0xff? O.o)

No no, both are exactly the same, as the source code suggests. Sorry if I didn’t explain properly.

The reason why .value is deprecated (and should be, imho) is because it’s not a good idea to hide (relatively) expensive computation behind something that sounds like it’s inexpensive (see AVOID using getters for computationally expensive operations in Dart style guide).

Also, the math behind the double to int is not that heavy. Maybe you found something more problematic with Dart math operations?

It’s possible. ¯\_(ツ)_/¯ The majority of the time was spent in _floatToInt8().

cc @mraleph – If you look at _floatToInt8() above (which is part of dart:ui’s Color class), do you think there may be something that makes it unnecessarily slow?

1 Like

Gotcha. Same guidelines in C# about properties.

Just out of curiosity. Can you try to replicate the Color code, but using .toInt() instead of .round() or .ceil()? I’m betting .round() is doing some nasty stuf (which is the only blackbox thing out there).

Hey, there
I’ve got an unrelated question here

How do you measure CPU time taken by a statement?

That’s a getter, so it shows up in CPU profile as part of the call stack.

So it means we should avoid using “GETTER” in general in dart? Or is it just in game niche to same the CPU cost.

Not at all! Getters are fine and I wouldn’t be surprised if they’re optimized away by the compiler in most cases (so it’s a simple value access).

Here, the deprecated Color.value getter is doing a lot of work (relatively speaking — we’re talking about hundreds of thousands of operations per frame here), which is against good practices for a getter.[1] Unless you have a similar scenario, you don’t need to worry about it.


  1. When reading the code, such as bitmap[pos] = myColor.value;, it looks like a simple value access. Contrast and compare with how bitmap[pos] = myColor.toARBG32(); reads. ↩︎

2 Likes

As I understood, Dart guidelines are the same I always used for C#, so I’ll try to explain it here, based on C# assumptions:

A getter (Type get name) should be the simplest method possible to get a value that already exists. A perfect get would only return a private variable. This is meant to protect the private variable against write by 3rd party users.

A getter should not compute values (that’s what Color.value does), should not throw exceptions (either explicit or by doing something that can result in exception (e.g.: dividing a number by a variable (that could potentially be 0)), etc.

So, getters should be cached. By using a property (get), you should be sure that no value is being computed, it is immutable (it won’t change if you call it more than once) and is fast.

This is also a good rule to cache things yourself: for instance, you know exactly what Theme.of(context) does just by looking at it? No. It’s a function, so, as a rule of thumb, you should cache it (i.e.: assign it once to a variable, such as final theme = Theme.of(context); in the build method and use theme whenever you need it). That’s the main difference between functions and getters: you should not worry caching getters because you know it is already a cached value (unless someone screw and ignores the rules and try to do some nasty computation in a getter, hence, the performance problem related here).

Filip could cache the .value in a variable and the issue is mitigated but, he could not have know this because, as the rules says, .value would not need it.

1 Like

Just for completeness: In my case, caching is actually not an option, because the color is only used once (it’s part of a custom retro 3D renderer, computing colors for 3D faces). So in my case, I switched to using a very simple substitute (since all I really needed from Color was the integer value anyway, and some way to make it brighter or darker).

As I said, this is very specific to my use case. 99+% people don’t need to worry about this at all, or —at worst — they can simply cache the value like @evaluator118 suggest.