Opaque shader too
diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs
index 823d0d8..f6a0432 100644
--- a/sparse_strips/vello_hybrid/src/render/webgl.rs
+++ b/sparse_strips/vello_hybrid/src/render/webgl.rs
@@ -56,7 +56,7 @@
     pixmap::Pixmap,
     tile::Tile,
 };
-use vello_sparse_shaders::{clear_slots, filters, render_strips};
+use vello_sparse_shaders::{clear_slots, filters, render_strips, render_strips_opaque};
 use web_sys::wasm_bindgen::{JsCast, JsValue};
 use web_sys::{
     WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram, WebGlRenderbuffer,
@@ -678,10 +678,14 @@
 /// Contains the WebGL programs and resources for rendering.
 #[derive(Debug)]
 struct WebGlPrograms {
-    /// Program for rendering wide tile commands.
+    /// Program for rendering wide tile commands (alpha pass and slot passes).
     strip_program: WebGlProgram,
-    /// Uniform locations for the strip program
+    /// Uniform locations for the strip program.
     strip_uniforms: StripUniforms,
+    /// Lightweight program for opaque strip pass (no alpha/blend/clip code).
+    opaque_strip_program: WebGlProgram,
+    /// Uniform locations for the opaque strip program.
+    opaque_strip_uniforms: OpaqueStripUniforms,
     /// Program for clearing slots in slot textures.
     clear_program: WebGlProgram,
     /// Uniform locations for the `clear_program`.
@@ -728,6 +732,19 @@
     gradient_texture: WebGlUniformLocation,
 }
 
+/// Uniform locations for `opaque_strip_program`.
+/// This shader omits alpha texture and clip input texture since opaque strips
+/// never need coverage sampling or clip slot reads.
+#[derive(Debug)]
+struct OpaqueStripUniforms {
+    config_vs_block_index: u32,
+    config_fs_block_index: u32,
+    atlas_texture_array: WebGlUniformLocation,
+    encoded_paints_texture_fs: WebGlUniformLocation,
+    encoded_paints_texture_vs: WebGlUniformLocation,
+    gradient_texture: WebGlUniformLocation,
+}
+
 /// Uniform locations for `clear_program`.
 #[derive(Debug)]
 struct ClearUniforms {
@@ -849,6 +866,11 @@
             render_strips::VERTEX_SOURCE,
             render_strips::FRAGMENT_SOURCE,
         );
+        let opaque_strip_program = create_shader_program(
+            &gl,
+            render_strips_opaque::VERTEX_SOURCE,
+            render_strips_opaque::FRAGMENT_SOURCE,
+        );
         let clear_program = create_shader_program(
             &gl,
             clear_slots::VERTEX_SOURCE,
@@ -859,6 +881,7 @@
         let filter_uniforms = get_filter_pass_uniforms(&gl, &filter_program);
 
         let strip_uniforms = get_strip_uniforms(&gl, &strip_program);
+        let opaque_strip_uniforms = get_opaque_strip_uniforms(&gl, &opaque_strip_program);
         let clear_uniforms = get_clear_uniforms(&gl, &clear_program);
 
         let resources = create_webgl_resources(&gl, image_cache, filter_context, slot_count);
@@ -869,6 +892,24 @@
 
         let encoded_paints_data = vec![0; (resources.max_texture_dimension_2d << 4) as usize];
 
+        // Set texture unit assignments once per program. These are per-program
+        // persistent state in WebGL2 and never need to be re-set.
+        gl.use_program(Some(&strip_program));
+        gl.uniform1i(Some(&strip_uniforms.alphas_texture), 0);
+        gl.uniform1i(Some(&strip_uniforms.clip_input_texture), 1);
+        gl.uniform1i(Some(&strip_uniforms.atlas_texture_array), 2);
+        gl.uniform1i(Some(&strip_uniforms.encoded_paints_texture_fs), 3);
+        gl.uniform1i(Some(&strip_uniforms.encoded_paints_texture_vs), 3);
+        gl.uniform1i(Some(&strip_uniforms.gradient_texture), 4);
+
+        gl.use_program(Some(&opaque_strip_program));
+        gl.uniform1i(Some(&opaque_strip_uniforms.atlas_texture_array), 2);
+        gl.uniform1i(Some(&opaque_strip_uniforms.encoded_paints_texture_fs), 3);
+        gl.uniform1i(Some(&opaque_strip_uniforms.encoded_paints_texture_vs), 3);
+        gl.uniform1i(Some(&opaque_strip_uniforms.gradient_texture), 4);
+
+        gl.use_program(None);
+
         gl.enable(WebGl2RenderingContext::BLEND);
         gl.blend_func(
             WebGl2RenderingContext::ONE,
@@ -877,10 +918,12 @@
 
         Self {
             strip_program,
+            opaque_strip_program,
             clear_program,
             filter_program,
             filter_uniforms,
             strip_uniforms,
+            opaque_strip_uniforms,
             clear_uniforms,
             resources,
             render_size: RenderSize {
@@ -1726,6 +1769,54 @@
     }
 }
 
+/// Get the uniform locations for the `render_strips_opaque` program.
+fn get_opaque_strip_uniforms(
+    gl: &WebGl2RenderingContext,
+    program: &WebGlProgram,
+) -> OpaqueStripUniforms {
+    let config_vs_name = render_strips_opaque::vertex::CONFIG;
+    let config_vs_block_index = gl.get_uniform_block_index(program, config_vs_name);
+
+    let config_fs_name = render_strips_opaque::fragment::CONFIG;
+    let config_fs_block_index = gl.get_uniform_block_index(program, config_fs_name);
+
+    debug_assert_ne!(
+        config_vs_block_index,
+        WebGl2RenderingContext::INVALID_INDEX,
+        "invalid uniform index"
+    );
+    debug_assert_ne!(
+        config_fs_block_index,
+        WebGl2RenderingContext::INVALID_INDEX,
+        "invalid uniform index"
+    );
+
+    gl.uniform_block_binding(program, config_vs_block_index, 0);
+    gl.uniform_block_binding(program, config_fs_block_index, 0);
+
+    let atlas_texture_array_name = render_strips_opaque::fragment::ATLAS_TEXTURE_ARRAY;
+    let encoded_paints_texture_fs_name = render_strips_opaque::fragment::ENCODED_PAINTS_TEXTURE;
+    let encoded_paints_texture_vs_name = render_strips_opaque::vertex::ENCODED_PAINTS_TEXTURE;
+    let gradient_texture_name = render_strips_opaque::fragment::GRADIENT_TEXTURE;
+
+    OpaqueStripUniforms {
+        config_vs_block_index,
+        config_fs_block_index,
+        atlas_texture_array: gl
+            .get_uniform_location(program, atlas_texture_array_name)
+            .unwrap(),
+        encoded_paints_texture_fs: gl
+            .get_uniform_location(program, encoded_paints_texture_fs_name)
+            .unwrap(),
+        encoded_paints_texture_vs: gl
+            .get_uniform_location(program, encoded_paints_texture_vs_name)
+            .unwrap(),
+        gradient_texture: gl
+            .get_uniform_location(program, gradient_texture_name)
+            .unwrap(),
+    }
+}
+
 /// Get the uniform locations for the `clear_slots` program.
 fn get_clear_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> ClearUniforms {
     let config_name = clear_slots::vertex::CONFIG;
@@ -2312,21 +2403,16 @@
             self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
         }
 
-        // Use the strip program.
-        self.gl.use_program(Some(&self.programs.strip_program));
-
         // Set up attributes.
         self.gl
             .bind_vertex_array(Some(&self.programs.resources.strip_vao));
 
-        // Bind textures.
+        // Bind textures to texture units (shared GL context state).
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
             Some(&self.programs.resources.alphas_texture),
         );
-        self.gl
-            .uniform1i(Some(&self.programs.strip_uniforms.alphas_texture), 0);
 
         let clip_texture_idx = match &target {
             StripPassRenderTarget::SlotTexture(1) => 0,
@@ -2337,41 +2423,24 @@
             WebGl2RenderingContext::TEXTURE_2D,
             Some(&self.programs.resources.slot_textures[clip_texture_idx]),
         );
-        self.gl
-            .uniform1i(Some(&self.programs.strip_uniforms.clip_input_texture), 1);
 
-        // Bind atlas texture array for image rendering
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE2);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D_ARRAY,
             Some(&self.programs.resources.atlas_texture_array.texture),
         );
-        self.gl
-            .uniform1i(Some(&self.programs.strip_uniforms.atlas_texture_array), 2);
 
-        // Bind encoded paints texture for image metadata
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE3);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
             Some(&self.programs.resources.encoded_paints_texture),
         );
-        self.gl.uniform1i(
-            Some(&self.programs.strip_uniforms.encoded_paints_texture_fs),
-            3,
-        );
-        self.gl.uniform1i(
-            Some(&self.programs.strip_uniforms.encoded_paints_texture_vs),
-            3,
-        );
 
-        // Bind gradient texture for gradient rendering
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE4);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
             Some(&self.programs.resources.gradient_texture),
         );
-        self.gl
-            .uniform1i(Some(&self.programs.strip_uniforms.gradient_texture), 4);
 
         let is_final_view =
             matches!(target, StripPassRenderTarget::Output(OutputTarget::FinalView));
@@ -2388,6 +2457,24 @@
             self.gl.enable(WebGl2RenderingContext::DEPTH_TEST);
             self.gl.depth_func(WebGl2RenderingContext::LEQUAL);
 
+            // Only pay the use_program switch to the opaque shader when the
+            // opaque list contains image or gradient strips that benefit from
+            // reduced register pressure. For all-solid-color opaque lists
+            // (the common case for text/SVG), the uber-shader's first branch
+            // is already trivially fast and the program switch costs more than
+            // it saves.
+            const PAINT_TYPE_MASK: u32 = 0x7 << 26;
+            let use_opaque_shader = opaque_strips
+                .iter()
+                .any(|s| s.paint_and_rect_flag & PAINT_TYPE_MASK != 0);
+
+            if use_opaque_shader {
+                self.gl
+                    .use_program(Some(&self.programs.opaque_strip_program));
+            } else {
+                self.gl.use_program(Some(&self.programs.strip_program));
+            }
+
             // Opaque pass: front-to-back, depth write ON, blend OFF.
             if !opaque_strips.is_empty() {
                 self.programs.upload_strips(self.gl, opaque_strips);
@@ -2403,6 +2490,11 @@
 
             // Alpha pass: back-to-front, depth test ON, depth write OFF, blend ON.
             if !alpha_strips.is_empty() {
+                // Switch back to the full shader only if we used the opaque
+                // shader above; otherwise we're already on strip_program.
+                if use_opaque_shader {
+                    self.gl.use_program(Some(&self.programs.strip_program));
+                }
                 self.programs.upload_strips(self.gl, alpha_strips);
                 self.gl.depth_mask(false);
                 self.gl.enable(WebGl2RenderingContext::BLEND);
@@ -2420,6 +2512,8 @@
             self.gl.enable(WebGl2RenderingContext::BLEND);
         } else {
             // Slot texture / intermediate: single draw with blending, no depth.
+            self.gl.use_program(Some(&self.programs.strip_program));
+
             // Combine both lists for upload.
             let all_strips: Vec<GpuStrip> = opaque_strips
                 .iter()
diff --git a/sparse_strips/vello_hybrid/src/render/wgpu.rs b/sparse_strips/vello_hybrid/src/render/wgpu.rs
index f934829..3fe1e90 100644
--- a/sparse_strips/vello_hybrid/src/render/wgpu.rs
+++ b/sparse_strips/vello_hybrid/src/render/wgpu.rs
@@ -1172,7 +1172,7 @@
                 },
                 fragment: Some(wgpu::FragmentState {
                     module: &strip_shader,
-                    entry_point: Some("fs_main"),
+                    entry_point: Some("fs_opaque"),
                     targets: &[Some(ColorTargetState {
                         format: strip_formats[i],
                         blend: None,
diff --git a/sparse_strips/vello_hybrid/src/schedule.rs b/sparse_strips/vello_hybrid/src/schedule.rs
index c252c12..4fc6aaa 100644
--- a/sparse_strips/vello_hybrid/src/schedule.rs
+++ b/sparse_strips/vello_hybrid/src/schedule.rs
@@ -1534,7 +1534,7 @@
                     Some(EncodedPaint::Image(img)) => {
                         !img.may_have_opacities
                             && img.sampler.alpha == 1.0
-                            && img.tint.is_none_or(|t| t.color.components[3] >= 1.0)
+                            && img.tint.is_none()
                     }
                     _ => false,
                 }
diff --git a/sparse_strips/vello_sparse_shaders/build.rs b/sparse_strips/vello_sparse_shaders/build.rs
index 90218ef..ebfa1e4 100644
--- a/sparse_strips/vello_sparse_shaders/build.rs
+++ b/sparse_strips/vello_sparse_shaders/build.rs
@@ -89,6 +89,15 @@
 
             let generated_code = compiled.to_generated_code(shader_name);
             writeln!(buf, "{generated_code}").unwrap();
+
+            // Compile the opaque variant of render_strips with the fs_opaque entry point.
+            if shader_name == "render_strips" {
+                let compiled_opaque =
+                    compile_wgsl_shader(shader_source, "vs_main", "fs_opaque");
+                let generated_opaque =
+                    compiled_opaque.to_generated_code("render_strips_opaque");
+                writeln!(buf, "{generated_opaque}").unwrap();
+            }
         }
     }
 }
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
index d028702..8c5de6f 100644
--- a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
+++ b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
@@ -551,6 +551,79 @@
     return vec4<f32>(0);
 }
 
+// Lightweight fragment shader for the opaque pass. Opaque strips are always
+// COLOR_SOURCE_PAYLOAD with fully opaque paint and never carry alpha-texture
+// coverage, rect-edge AA fractions, clip-slot reads, or blend compositing.
+// Keeping this as a separate entry point avoids compiling blend/clip/alpha
+// code into the opaque pipeline, reducing register pressure on low-tier GPUs.
+@fragment
+fn fs_opaque(in: VertexOutput) -> @location(0) vec4<f32> {
+    let paint_type = (in.paint_and_rect_flag >> 26u) & 0x7u;
+
+    if paint_type == PAINT_TYPE_SOLID {
+        return unpack4x8unorm(in.payload);
+    } else if paint_type == PAINT_TYPE_IMAGE {
+        let paint_tex_idx = in.paint_and_rect_flag & PAINT_TEXTURE_INDEX_MASK;
+        let encoded_image = unpack_encoded_image(paint_tex_idx);
+        let image_offset = encoded_image.image_offset;
+        let image_size = encoded_image.image_size;
+        let local_xy = in.sample_xy - image_offset;
+        let offset = 0.00001;
+        let extended_xy = vec2<f32>(
+            extend_mode(local_xy.x + offset, encoded_image.extend_modes.x, image_size.x),
+            extend_mode(local_xy.y + offset, encoded_image.extend_modes.y, image_size.y)
+        );
+
+        let final_xy = image_offset + extended_xy;
+        let inv_atlas_dim = 1.0 / f32(1u << config.atlas_dim_bits);
+        let uv = clamp(final_xy * inv_atlas_dim, in.uv_bounds.xy, in.uv_bounds.zw);
+        return textureSample(atlas_texture_array, atlas_sampler, uv, i32(encoded_image.atlas_index));
+    } else if paint_type == PAINT_TYPE_LINEAR_GRADIENT {
+        let paint_tex_idx = in.paint_and_rect_flag & PAINT_TEXTURE_INDEX_MASK;
+        let linear_gradient = unpack_linear_gradient(paint_tex_idx);
+        let fragment_pos = in.sample_xy;
+        let grad_pos = linear_gradient.transform * fragment_pos + linear_gradient.translate;
+        let t_value = grad_pos.x + 0.00001;
+        return sample_gradient_lut(
+            t_value,
+            linear_gradient.extend_mode,
+            linear_gradient.gradient_start,
+            linear_gradient.texture_width,
+            true
+        );
+    } else if paint_type == PAINT_TYPE_RADIAL_GRADIENT {
+        let paint_tex_idx = in.paint_and_rect_flag & PAINT_TEXTURE_INDEX_MASK;
+        let radial_gradient = unpack_radial_gradient(paint_tex_idx);
+        let fragment_pos = in.sample_xy;
+        let grad_pos = radial_gradient.transform * fragment_pos + radial_gradient.translate;
+        let gradient_result = calculate_radial_gradient(grad_pos, radial_gradient);
+        return sample_gradient_lut(
+            gradient_result.t_value,
+            radial_gradient.extend_mode,
+            radial_gradient.gradient_start,
+            radial_gradient.texture_width,
+            gradient_result.is_valid
+        );
+    } else if paint_type == PAINT_TYPE_SWEEP_GRADIENT {
+        let paint_tex_idx = in.paint_and_rect_flag & PAINT_TEXTURE_INDEX_MASK;
+        let sweep_gradient = unpack_sweep_gradient(paint_tex_idx);
+        let fragment_pos = in.sample_xy;
+        var grad_pos = sweep_gradient.transform * fragment_pos + sweep_gradient.translate;
+        grad_pos = select(grad_pos, vec2(0.0), abs(grad_pos) < vec2(NEARLY_ZERO_TOLERANCE));
+        let unit_angle = xy_to_unit_angle(grad_pos.x, grad_pos.y);
+        let angle = unit_angle * TWO_PI;
+        let t_value = (angle - sweep_gradient.start_angle) * sweep_gradient.inv_angle_delta;
+        return sample_gradient_lut(
+            t_value,
+            sweep_gradient.extend_mode,
+            sweep_gradient.gradient_start,
+            sweep_gradient.texture_width,
+            true
+        );
+    }
+    return vec4<f32>(0);
+}
+
 // Apply color mixing and composition. Both input and output colors are premultiplied RGB.
 // Referenced from:
 //   <https://github.com/linebender/vello/blob/b0e2e598ac62c7b3d04d8660e7b1b7659b596970/vello_shaders/shader/shared/blend.wgsl#L288-L310>