Opening
You write a TextView in XML or call Text in Compose, and text appears on screen. Simple, right? But think about what had to happen. Something had to figure out how big that text should be. Something had to figure out where to put it. And something had to figure out what color each pixel should be and send that to the display. This is the rendering pipeline, and it is one of the most elegant pieces of engineering in Android. Three passes — measure, layout, draw — turning a tree of objects in memory into a grid of colored pixels on a physical display. Today we are going to walk through each pass, understand what triggers them, see what makes them slow, and learn why the way you structure your views has a direct impact on performance. If you have ever wondered why ConstraintLayout is faster than nested LinearLayouts, this video will make it crystal clear.
The View Tree
Before we talk about rendering, let us talk about what is being rendered. Your UI is a tree of View objects. At the top is the DecorView — the root that the system creates. Inside it is your layout. A LinearLayout containing a few TextViews. Or a ConstraintLayout with a dozen children. Or a RecyclerView whose children are dynamically created. Every visible element on screen is a View or a ViewGroup in this tree. And when it is time to render a frame, the system traverses this tree three times — once for measure, once for layout, once for draw. The larger and deeper your tree, the more work each traversal requires. This is the fundamental reason flat hierarchies are faster: fewer nodes to visit, fewer operations per frame.
The Measure Pass
The first pass is measure. The question being answered is: how big should each view be? This starts at the root. The root view is told by the system: you have this much space available — the full screen, minus the status bar and navigation bar. The root then asks each of its children: given this much space, how big do you want to be? Each child calculates its desired size based on its content, its layout parameters, and the constraints from its parent. A TextView measures its text, accounts for padding, and reports back. An ImageView measures its image, considers the scaleType, and reports back. A ViewGroup measures all its children first, then calculates its own size based on how its children arranged themselves. The key thing to understand is that measure works top-down. The parent tells the child the constraints, and the child reports back its desired size. This happens for every single view in the tree, every single frame that requires remeasurement.
The Layout Pass
Once every view knows how big it wants to be, the second pass begins — layout. The question now is: where does each view go? The root view is positioned at the top left of the screen. Then it positions each of its children within its own bounds. A vertical LinearLayout places its first child at the top, then the second child below it, then the third below that. A ConstraintLayout uses its constraint equations to calculate exact positions for each child. Each view receives its final coordinates — left, top, right, bottom — defining its rectangle on screen. Like measure, layout is a tree traversal. The parent positions the children, and if a child is itself a ViewGroup, it positions its own children within its allocated rectangle. At the end of this pass, every view knows exactly where it sits on screen and exactly how big it is.
The Draw Pass
Now comes the fun part — drawing. Each view is told to draw itself into a Canvas. A Canvas is an abstraction over a drawing surface. The view can draw text, shapes, images, colors — whatever it needs to express itself visually. A TextView draws its text string. A background drawable draws a filled rectangle. An ImageView draws its bitmap. The draw traversal goes through the tree, and each view draws itself, then its children draw themselves on top. Views that appear later in the tree are drawn on top of views that appear earlier — that is how the z-ordering works. But here is the important detail — with hardware acceleration, which is on by default since API 14, views are not drawing directly to the screen. They are recording their drawing commands into a display list — a serialized set of instructions like "draw a rectangle here" and "draw text there." That display list is then handed to the RenderThread, which sends it to the GPU for actual rendering. This is a big deal because it means the main thread does not have to wait for the GPU.
Invalidate and RequestLayout
What triggers these passes? Two methods you should know: invalidate and requestLayout. When you call invalidate on a view, you are saying "my appearance has changed — I need to redraw." Maybe the text color changed, or a background color was updated. Invalidate triggers only the draw pass. It does not trigger measure or layout, because the view's size and position have not changed. When you call requestLayout, you are saying "my size or position may have changed — I need to be remeasured and repositioned." Maybe the text got longer, or padding was changed. RequestLayout triggers the full pipeline — measure, layout, and draw. This distinction matters for performance. Invalidate is relatively cheap because it skips two of the three passes. RequestLayout is expensive because it starts from the beginning. And requestLayout does not just affect the view you called it on — it propagates up the tree to the root, because if a child changes size, its parent might need to change size too, and the parent's parent, all the way up.
Double Taxation
Now let us talk about one of the sneakiest performance problems — double taxation. Some layouts need to measure their children more than once to figure out the correct sizes. A classic example is a LinearLayout with layout_weight. The first measure pass calculates how big children want to be naturally. Then the LinearLayout sees there is leftover space that needs to be distributed according to the weights, so it measures those children a second time with the adjusted sizes. Each child measured twice means the work doubles. But it gets worse. If you nest a weighted LinearLayout inside another weighted LinearLayout, the inner one's children get measured four times — two for the outer, times two for the inner. Add another level and it is eight times. This is exponential growth. Three levels of weighted LinearLayouts means each leaf view is measured eight times per frame. This is exactly why deeply nested layouts with measurement-heavy parents cause jank. ConstraintLayout avoids this by solving all constraints in a single pass, which is why it exists.
The RenderThread and GPU Rendering
Let me go deeper on what happens after the draw pass. When your views record their drawing commands into display lists, those display lists are handed to the RenderThread. The RenderThread is a separate thread — not the main thread — that is responsible for talking to the GPU. It takes the display lists, optimizes them, and issues OpenGL or Vulkan commands to the GPU. The GPU then rasterizes everything — turning those vector drawing commands into actual pixels in a framebuffer. This architecture is great for performance because the main thread can start working on the next frame while the RenderThread is still rendering the current one. They work in parallel. If your draw commands are simple and your display lists are small, the RenderThread finishes quickly and the frame makes it to the display on time. If your views are complex with lots of custom drawing, gradients, shadows, and clipping, the RenderThread takes longer and you might miss the frame deadline.
Overdraw
Overdraw is when the GPU draws the same pixel multiple times in a single frame. Imagine you have a white background on your Activity, then a white background on a fragment, then a white background on a card, then text on the card. That pixel under the text was painted four times — three layers of white backgrounds and then the text. Each time a pixel is painted, the GPU does work. Painting it four times means four times the GPU work for that pixel. Multiply that by a million pixels and you have a real performance cost. Android has a developer option called "Debug GPU Overdraw" that color-codes your screen. Blue means one overdraw — two draws total. Green means two overdraws. Pink means three. Red means four or more. You want to minimize the red. The fix is usually to remove unnecessary backgrounds. If a parent is already painting a white background, the child does not need one too. Remove the redundant android background attributes. In Compose, be careful with Modifier.background — only apply it where truly needed.
Compose: Different Pipeline, Same Concepts
If you are using Jetpack Compose, the pipeline looks different on the surface but follows the same principles. Instead of View objects, you have composable functions that emit a Composition — a tree of layout nodes. Instead of measure and layout as separate passes, Compose does them together in a single traversal, which is actually more efficient. Compose also prevents double taxation by design — it uses intrinsic measurements that are resolved in a single pass. The draw phase works similarly — each layout node records drawing commands. And the RenderThread and GPU still do the final rendering. But the biggest difference is recomposition. When state changes in Compose, the framework does not remeasure the entire tree. It only re-executes the composable functions that read the changed state. Then only the affected parts of the layout tree are remeasured and redrawn. This is called smart recomposition, and it can be significantly more efficient than the View system, where invalidate and requestLayout can propagate widely through the tree.
Why Flat Hierarchies Are Faster
Let me bring it all together. Every frame, the system traverses your view tree for measure, layout, and draw. A flat tree with 20 children has 20 nodes to visit. A deeply nested tree with the same 20 views might have 50 total nodes because of all the intermediate ViewGroups — the LinearLayouts inside FrameLayouts inside RelativeLayouts. More nodes means more work per traversal. But the real danger is measurement multiplication. Each level of nesting can multiply the number of measurements. Three levels of nested containers that each measure twice means each leaf is measured eight times. Flat hierarchies avoid this multiplication entirely. ConstraintLayout lets you express complex positioning relationships without nesting, solving everything in one measurement pass. In Compose, the layout system is inherently flatter because you do not need wrapper containers for simple arrangements. This is not premature optimization — it is understanding the rendering pipeline and working with it rather than against it.
Recap and What is Next
Let us recap the rendering pipeline. Your UI is a tree of views. Each frame, the tree is traversed three times: measure determines size, layout determines position, draw records rendering commands. Invalidate triggers only draw. RequestLayout triggers the full pipeline. Double taxation means some layouts measure children multiple times, and nesting amplifies this exponentially. The RenderThread sends display lists to the GPU for final pixel rendering. Overdraw means painting the same pixel multiple times, wasting GPU cycles. Compose follows the same concepts with a more efficient single-pass layout and smart recomposition. Flat hierarchies minimize traversal work and avoid measurement multiplication. In the next video, we are going to look at what happens when something goes seriously wrong — when your main thread gets so backed up that the system intervenes. That is ANR — Application Not Responding. See you there.