.
diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs
index ff5f8b8..7b1d3e6 100644
--- a/sparse_strips/vello_hybrid/src/render/webgl.rs
+++ b/sparse_strips/vello_hybrid/src/render/webgl.rs
@@ -45,8 +45,10 @@
 use alloc::vec::Vec;
 use bytemuck::{Pod, Zeroable};
 use core::fmt::Debug;
+use core::ops::Deref;
 #[cfg(feature = "text")]
 use glifo::{GLYPH_PADDING, PendingClearRect};
+use resource::{Buffer, FragmentShader, Framebuffer, Program, Texture, VertexArray, VertexShader};
 use vello_common::image_cache::{ImageCache, ImageResource};
 use vello_common::multi_atlas::{AtlasConfig, AtlasId};
 use vello_common::render_graph::LayerId;
@@ -61,7 +63,7 @@
 use vello_sparse_shaders::{clear_slots, filters, render_strips};
 use web_sys::wasm_bindgen::{JsCast, JsValue};
 use web_sys::{
-    WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram, WebGlTexture,
+    WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram, WebGlShader, WebGlTexture,
     WebGlUniformLocation, WebGlVertexArrayObject,
 };
 
@@ -273,7 +275,7 @@
             .resources
             .atlas_render_framebuffer
             .take()
-            .unwrap_or_else(|| self.gl.create_framebuffer().unwrap());
+            .unwrap_or_else(|| Framebuffer::new(&self.gl));
         self.gl.bind_framebuffer(
             WebGl2RenderingContext::FRAMEBUFFER,
             Some(&atlas_framebuffer),
@@ -498,7 +500,7 @@
     /// Clear a specific region of the atlas texture array.
     fn clear_atlas_region(&mut self, atlas_id: AtlasId, offset: [u32; 2], width: u32, height: u32) {
         let _state_guard = WebGlStateGuard::for_clear_atlas_region(&self.gl);
-        let temp_framebuffer = self.gl.create_framebuffer().unwrap();
+        let temp_framebuffer = Framebuffer::new(&self.gl);
 
         // Bind our temporary framebuffer
         self.gl
@@ -530,9 +532,6 @@
         // Clear the region to transparent (0, 0, 0, 0)
         self.gl.clear_color(0.0, 0.0, 0.0, 0.0);
         self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
-
-        // Clean up temporary framebuffer
-        self.gl.delete_framebuffer(Some(&temp_framebuffer));
     }
 
     fn prepare_gpu_encoded_paints(
@@ -700,15 +699,15 @@
 #[derive(Debug)]
 struct WebGlPrograms {
     /// Program for rendering wide tile commands.
-    strip_program: WebGlProgram,
+    strip_program: Program,
     /// Uniform locations for the strip program
     strip_uniforms: StripUniforms,
     /// Program for clearing slots in slot textures.
-    clear_program: WebGlProgram,
+    clear_program: Program,
     /// Uniform locations for the `clear_program`.
     clear_uniforms: ClearUniforms,
     /// Program for filter passes.
-    filter_program: WebGlProgram,
+    filter_program: Program,
     /// Uniform locations for the filter program.
     filter_uniforms: FilterPassUniforms,
     /// WebGL resources for rendering.
@@ -762,42 +761,42 @@
 #[derive(Debug)]
 struct WebGlResources {
     /// VAO for strip rendering.
-    strip_vao: WebGlVertexArrayObject,
+    strip_vao: VertexArray,
     /// Buffer for [`GpuStrip`] data.
-    strips_buffer: WebGlBuffer,
+    strips_buffer: Buffer,
     /// Texture for alpha values (used by both view and slot rendering).
-    alphas_texture: WebGlTexture,
+    alphas_texture: Texture,
     /// Height of alpha texture.
     alpha_texture_height: u32,
     /// Texture array for atlas data (multiple atlases supported)
     atlas_texture_array: WebGlTextureArray,
     /// Encoded paints texture for image metadata.
-    encoded_paints_texture: WebGlTexture,
+    encoded_paints_texture: Texture,
     /// Height of encoded paints texture.
     encoded_paints_texture_height: u32,
     /// Gradient texture for gradient ramp data.
-    gradient_texture: WebGlTexture,
+    gradient_texture: Texture,
     /// Height of gradient texture.
     gradient_texture_height: u32,
 
     /// Config buffer for rendering wide tile commands into the view texture.
-    view_config_buffer: WebGlBuffer,
+    view_config_buffer: Buffer,
     /// Config buffer for rendering wide tile commands into a slot texture.
-    slot_config_buffer: WebGlBuffer,
+    slot_config_buffer: Buffer,
 
     /// Buffer for slot indices used in `clear_slots`.
-    clear_slot_indices_buffer: WebGlBuffer,
+    clear_slot_indices_buffer: Buffer,
     /// VAO for clear slots program.
-    clear_vao: WebGlVertexArrayObject,
+    clear_vao: VertexArray,
     /// Config buffer for clear program.
-    clear_config_buffer: WebGlBuffer,
+    clear_config_buffer: Buffer,
 
-    view_framebuffer_override: Option<WebGlFramebuffer>,
+    view_framebuffer_override: Option<Framebuffer>,
 
     /// Slot textures.
-    slot_textures: [WebGlTexture; 2],
+    slot_textures: [Texture; 2],
     /// Framebuffers for slot textures.
-    slot_framebuffers: [WebGlFramebuffer; 2],
+    slot_framebuffers: [Framebuffer; 2],
 
     /// Cached result from querying `WebGl2RenderingContext::MAX_TEXTURE_SIZE` which is a blocking
     /// WebGL call.
@@ -809,26 +808,26 @@
 
     /// Cached framebuffer for rendering into an atlas layer in `render_to_atlas`.
     /// Reused to avoid create/delete overhead on every call.
-    atlas_render_framebuffer: Option<WebGlFramebuffer>,
+    atlas_render_framebuffer: Option<Framebuffer>,
 
     /// Cached framebuffer for filter passes that write back to the main atlas.
     /// Reused to avoid create/delete overhead on every filter application.
-    filter_main_atlas_framebuffer: Option<WebGlFramebuffer>,
+    filter_main_atlas_framebuffer: Option<Framebuffer>,
 
     /// Individual 2D textures for filter intermediate results.
-    filter_atlas_textures: Vec<WebGlTexture>,
+    filter_atlas_textures: Vec<Texture>,
     /// Framebuffers for each filter atlas texture.
-    filter_atlas_framebuffers: Vec<WebGlFramebuffer>,
+    filter_atlas_framebuffers: Vec<Framebuffer>,
     /// RGBA32UI texture storing filter parameters.
-    filter_data_texture: WebGlTexture,
+    filter_data_texture: Texture,
     /// Current height of filter data texture.
     filter_data_texture_height: u32,
     /// Per-instance vertex data buffer for filter draws.
-    filter_instance_buffer: WebGlBuffer,
+    filter_instance_buffer: Buffer,
     /// VAO for filter rendering.
-    filter_vao: WebGlVertexArrayObject,
+    filter_vao: VertexArray,
     /// Config buffer for rendering filter layers.
-    filter_config_buffer: WebGlBuffer,
+    filter_config_buffer: Buffer,
     /// Cached atlas width for creating new filter atlas textures.
     filter_atlas_width: u32,
     /// Cached atlas height for creating new filter atlas textures.
@@ -957,12 +956,8 @@
             // Replace the old resources
             self.resources.atlas_texture_array = new_atlas_texture_array;
             // Cached FBOs were attached to the old texture; drop them so we recreate on next use.
-            if let Some(fb) = self.resources.atlas_render_framebuffer.take() {
-                gl.delete_framebuffer(Some(&fb));
-            }
-            if let Some(fb) = self.resources.filter_main_atlas_framebuffer.take() {
-                gl.delete_framebuffer(Some(&fb));
-            }
+            let _ = self.resources.atlas_render_framebuffer.take();
+            let _ = self.resources.filter_main_atlas_framebuffer.take();
         }
     }
 
@@ -1369,7 +1364,10 @@
     fn clear_view_framebuffer(&mut self, gl: &WebGl2RenderingContext) {
         gl.bind_framebuffer(
             WebGl2RenderingContext::FRAMEBUFFER,
-            self.resources.view_framebuffer_override.as_ref(),
+            self.resources
+                .view_framebuffer_override
+                .as_ref()
+                .map(|f| f.deref()),
         );
         gl.clear_color(0.0, 0.0, 0.0, 0.0);
         gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
@@ -1570,11 +1568,9 @@
     gl: &WebGl2RenderingContext,
     vertex_src: &str,
     fragment_src: &str,
-) -> WebGlProgram {
+) -> Program {
     // Compile vertex shader.
-    let vertex_shader = gl
-        .create_shader(WebGl2RenderingContext::VERTEX_SHADER)
-        .unwrap();
+    let vertex_shader = VertexShader::new(gl);
     gl.shader_source(&vertex_shader, vertex_src);
     gl.compile_shader(&vertex_shader);
 
@@ -1590,9 +1586,7 @@
     }
 
     // Compile fragment shader.
-    let fragment_shader = gl
-        .create_shader(WebGl2RenderingContext::FRAGMENT_SHADER)
-        .unwrap();
+    let fragment_shader = FragmentShader::new(gl);
     gl.shader_source(&fragment_shader, fragment_src);
     gl.compile_shader(&fragment_shader);
 
@@ -1608,7 +1602,7 @@
     }
 
     // Create and link the program.
-    let program = gl.create_program().unwrap();
+    let program = Program::new(gl);
     gl.attach_shader(&program, &vertex_shader);
     gl.attach_shader(&program, &fragment_shader);
     gl.link_program(&program);
@@ -1624,14 +1618,11 @@
         panic!("Failed to link program: {info}");
     }
 
-    gl.delete_shader(Some(&vertex_shader));
-    gl.delete_shader(Some(&fragment_shader));
-
     program
 }
 
 /// Get the  uniform locations for the `render_strips` program.
-fn get_strip_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> StripUniforms {
+fn get_strip_uniforms(gl: &WebGl2RenderingContext, program: &Program) -> StripUniforms {
     let config_vs_name = render_strips::vertex::CONFIG;
     let config_vs_block_index = gl.get_uniform_block_index(program, config_vs_name);
 
@@ -1686,7 +1677,7 @@
 }
 
 /// Get the uniform locations for the `clear_slots` program.
-fn get_clear_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> ClearUniforms {
+fn get_clear_uniforms(gl: &WebGl2RenderingContext, program: &Program) -> ClearUniforms {
     let config_name = clear_slots::vertex::CONFIG;
     let config_block_index = gl.get_uniform_block_index(program, config_name);
 
@@ -1702,10 +1693,7 @@
     ClearUniforms { config_block_index }
 }
 
-fn get_filter_pass_uniforms(
-    gl: &WebGl2RenderingContext,
-    program: &WebGlProgram,
-) -> FilterPassUniforms {
+fn get_filter_pass_uniforms(gl: &WebGl2RenderingContext, program: &Program) -> FilterPassUniforms {
     let filter_data = gl
         .get_uniform_location(program, filters::fragment::FILTER_DATA)
         .unwrap();
@@ -1722,12 +1710,8 @@
     }
 }
 
-fn create_filter_atlas_texture(
-    gl: &WebGl2RenderingContext,
-    width: u32,
-    height: u32,
-) -> WebGlTexture {
-    let texture = gl.create_texture().unwrap();
+fn create_filter_atlas_texture(gl: &WebGl2RenderingContext, width: u32, height: u32) -> Texture {
+    let texture = Texture::new(gl);
     gl.active_texture(WebGl2RenderingContext::TEXTURE0);
     gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture));
     gl.tex_parameteri(
@@ -1804,18 +1788,18 @@
 }
 
 /// Create a texture with nearest neighbor sampling and clamp-to-edge wrapping.
-fn create_texture(gl: &WebGl2RenderingContext) -> WebGlTexture {
+fn create_texture(gl: &WebGl2RenderingContext) -> Texture {
     create_texture_inner(gl, WebGl2RenderingContext::TEXTURE_2D)
 }
 
 /// Create a texture array with nearest neighbor sampling and
 /// clamp-to-edge wrapping.
-fn create_texture_array(gl: &WebGl2RenderingContext) -> WebGlTexture {
+fn create_texture_array(gl: &WebGl2RenderingContext) -> Texture {
     create_texture_inner(gl, WebGl2RenderingContext::TEXTURE_2D_ARRAY)
 }
 
-fn create_texture_inner(gl: &WebGl2RenderingContext, target: u32) -> WebGlTexture {
-    let texture = gl.create_texture().unwrap();
+fn create_texture_inner(gl: &WebGl2RenderingContext, target: u32) -> Texture {
+    let texture = Texture::new(gl);
     gl.active_texture(WebGl2RenderingContext::TEXTURE0);
     gl.bind_texture(target, Some(&texture));
     // The filter and wrap modes are irrelevant because the shader
@@ -1855,16 +1839,16 @@
     filter_context: &FilterContext,
     slot_count: usize,
 ) -> WebGlResources {
-    let strip_vao = gl.create_vertex_array().unwrap();
-    let clear_vao = gl.create_vertex_array().unwrap();
-    let filter_vao = gl.create_vertex_array().unwrap();
-    let filter_instance_buffer = gl.create_buffer().unwrap();
+    let strip_vao = VertexArray::new(gl);
+    let clear_vao = VertexArray::new(gl);
+    let filter_vao = VertexArray::new(gl);
+    let filter_instance_buffer = Buffer::new(gl);
 
-    let strips_buffer = gl.create_buffer().unwrap();
-    let view_config_buffer = gl.create_buffer().unwrap();
-    let slot_config_buffer = gl.create_buffer().unwrap();
-    let clear_slot_indices_buffer = gl.create_buffer().unwrap();
-    let clear_config_buffer = gl.create_buffer().unwrap();
+    let strips_buffer = Buffer::new(gl);
+    let view_config_buffer = Buffer::new(gl);
+    let slot_config_buffer = Buffer::new(gl);
+    let clear_slot_indices_buffer = Buffer::new(gl);
+    let clear_config_buffer = Buffer::new(gl);
 
     // Create and configure alpha texture.
     let alphas_texture = create_texture(gl);
@@ -1888,12 +1872,12 @@
     let gradient_texture = create_texture(gl);
 
     // Create slot textures and framebuffers.
-    let slot_textures: [WebGlTexture; 2] = [
+    let slot_textures: [Texture; 2] = [
         create_slot_texture(gl, slot_count),
         create_slot_texture(gl, slot_count),
     ];
 
-    let slot_framebuffers: [WebGlFramebuffer; 2] = [
+    let slot_framebuffers: [Framebuffer; 2] = [
         create_framebuffer_for_texture(gl, &slot_textures[0]),
         create_framebuffer_for_texture(gl, &slot_textures[1]),
     ];
@@ -1901,7 +1885,7 @@
     let max_texture_dimension_2d = get_max_texture_dimension_2d(gl);
 
     let filter_data_texture = create_texture(gl);
-    let filter_config_buffer = gl.create_buffer().unwrap();
+    let filter_config_buffer = Buffer::new(gl);
 
     let AtlasConfig {
         atlas_size: (filter_atlas_width, filter_atlas_height),
@@ -1970,7 +1954,7 @@
 }
 
 /// Create a texture for slot rendering.
-fn create_slot_texture(gl: &WebGl2RenderingContext, slot_count: usize) -> WebGlTexture {
+fn create_slot_texture(gl: &WebGl2RenderingContext, slot_count: usize) -> Texture {
     let texture = create_texture(gl);
 
     gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view(
@@ -1990,11 +1974,8 @@
 }
 
 /// Create a framebuffer for a texture.
-fn create_framebuffer_for_texture(
-    gl: &WebGl2RenderingContext,
-    texture: &WebGlTexture,
-) -> WebGlFramebuffer {
-    let framebuffer = gl.create_framebuffer().unwrap();
+fn create_framebuffer_for_texture(gl: &WebGl2RenderingContext, texture: &Texture) -> Framebuffer {
+    let framebuffer = Framebuffer::new(gl);
     gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&framebuffer));
 
     gl.framebuffer_texture_2d(
@@ -2161,7 +2142,7 @@
             StripPassRenderTarget::Root(_) => {
                 self.gl.bind_framebuffer(
                     WebGl2RenderingContext::FRAMEBUFFER,
-                    self.programs.resources.view_framebuffer_override.as_ref(),
+                    self.programs.resources.view_framebuffer_override.as_deref(),
                 );
                 let width = self.programs.render_size.width;
                 let height = self.programs.render_size.height;
@@ -2443,9 +2424,9 @@
                         .programs
                         .resources
                         .filter_main_atlas_framebuffer
-                        .get_or_insert_with(|| self.gl.create_framebuffer().unwrap());
+                        .get_or_insert_with(|| Framebuffer::new(&self.gl));
                     self.gl
-                        .bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(fb));
+                        .bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&*fb));
                     self.gl.framebuffer_texture_layer(
                         WebGl2RenderingContext::FRAMEBUFFER,
                         WebGl2RenderingContext::COLOR_ATTACHMENT0,
@@ -2669,14 +2650,14 @@
 #[derive(Debug)]
 struct WebGlTextureArray {
     /// The WebGL texture array.
-    texture: WebGlTexture,
+    texture: Texture,
     /// The size of the texture array.
     size: WebGlTextureSize,
 }
 
 impl WebGlTextureArray {
     /// Create a new WebGL texture array wrapper.
-    fn new(texture: WebGlTexture, width: u32, height: u32, depth_or_array_layers: u32) -> Self {
+    fn new(texture: Texture, width: u32, height: u32, depth_or_array_layers: u32) -> Self {
         Self {
             texture,
             size: WebGlTextureSize {
@@ -2714,7 +2695,7 @@
     copy_size: [u32; 2],
 ) {
     let _state_guard = WebGlStateGuard::for_texture_copy(gl);
-    let read_framebuffer = gl.create_framebuffer().unwrap();
+    let read_framebuffer = Framebuffer::new(gl);
 
     // Bind destination texture array
     gl.active_texture(WebGl2RenderingContext::TEXTURE0);
@@ -2744,9 +2725,6 @@
         copy_size[0] as i32,
         copy_size[1] as i32,
     );
-
-    // Clean up
-    gl.delete_framebuffer(Some(&read_framebuffer));
 }
 
 // Upload the data to the currently bound texture assuming a RGBA32UI format.
@@ -2789,3 +2767,173 @@
     )
     .unwrap();
 }
+
+mod resource {
+    use core::ops::Deref;
+
+    use super::{
+        WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram, WebGlShader,
+        WebGlTexture, WebGlVertexArrayObject,
+    };
+
+    pub(super) trait GlResource {
+        const LABEL: &'static str;
+
+        fn create(gl: &WebGl2RenderingContext) -> Option<Self>
+        where
+            Self: Sized;
+
+        fn delete(gl: &WebGl2RenderingContext, raw: &Self);
+    }
+
+    #[derive(Debug)]
+    pub(super) struct Resource<T: GlResource> {
+        gl: WebGl2RenderingContext,
+        raw: T,
+    }
+
+    impl<T: GlResource> Resource<T> {
+        pub(super) fn new(gl: &WebGl2RenderingContext) -> Self {
+            let raw =
+                T::create(gl).unwrap_or_else(|| panic!("failed to create WebGL {}", T::LABEL));
+            Self::from_raw(gl, raw)
+        }
+
+        pub(super) fn from_raw(gl: &WebGl2RenderingContext, raw: T) -> Self {
+            Self {
+                gl: gl.clone(),
+                raw,
+            }
+        }
+    }
+
+    impl<T: GlResource> Drop for Resource<T> {
+        fn drop(&mut self) {
+            T::delete(&self.gl, &self.raw);
+        }
+    }
+
+    impl<T: GlResource> Deref for Resource<T> {
+        type Target = T;
+
+        fn deref(&self) -> &Self::Target {
+            &self.raw
+        }
+    }
+
+    #[derive(Debug)]
+    pub(super) struct RawVertexShader(WebGlShader);
+
+    impl Deref for RawVertexShader {
+        type Target = WebGlShader;
+
+        fn deref(&self) -> &Self::Target {
+            &self.0
+        }
+    }
+
+    #[derive(Debug)]
+    pub(super) struct RawFragmentShader(WebGlShader);
+
+    impl Deref for RawFragmentShader {
+        type Target = WebGlShader;
+
+        fn deref(&self) -> &Self::Target {
+            &self.0
+        }
+    }
+
+    impl GlResource for WebGlTexture {
+        const LABEL: &'static str = "texture";
+
+        fn create(gl: &WebGl2RenderingContext) -> Option<Self> {
+            gl.create_texture()
+        }
+
+        fn delete(gl: &WebGl2RenderingContext, raw: &Self) {
+            gl.delete_texture(Some(raw));
+        }
+    }
+
+    impl GlResource for WebGlBuffer {
+        const LABEL: &'static str = "buffer";
+
+        fn create(gl: &WebGl2RenderingContext) -> Option<Self> {
+            gl.create_buffer()
+        }
+
+        fn delete(gl: &WebGl2RenderingContext, raw: &Self) {
+            gl.delete_buffer(Some(raw));
+        }
+    }
+
+    impl GlResource for WebGlFramebuffer {
+        const LABEL: &'static str = "framebuffer";
+
+        fn create(gl: &WebGl2RenderingContext) -> Option<Self> {
+            gl.create_framebuffer()
+        }
+
+        fn delete(gl: &WebGl2RenderingContext, raw: &Self) {
+            gl.delete_framebuffer(Some(raw));
+        }
+    }
+
+    impl GlResource for WebGlProgram {
+        const LABEL: &'static str = "program";
+
+        fn create(gl: &WebGl2RenderingContext) -> Option<Self> {
+            gl.create_program()
+        }
+
+        fn delete(gl: &WebGl2RenderingContext, raw: &Self) {
+            gl.delete_program(Some(raw));
+        }
+    }
+
+    impl GlResource for RawVertexShader {
+        const LABEL: &'static str = "vertex shader";
+
+        fn create(gl: &WebGl2RenderingContext) -> Option<Self> {
+            gl.create_shader(WebGl2RenderingContext::VERTEX_SHADER)
+                .map(Self)
+        }
+
+        fn delete(gl: &WebGl2RenderingContext, raw: &Self) {
+            gl.delete_shader(Some(raw));
+        }
+    }
+
+    impl GlResource for RawFragmentShader {
+        const LABEL: &'static str = "fragment shader";
+
+        fn create(gl: &WebGl2RenderingContext) -> Option<Self> {
+            gl.create_shader(WebGl2RenderingContext::FRAGMENT_SHADER)
+                .map(Self)
+        }
+
+        fn delete(gl: &WebGl2RenderingContext, raw: &Self) {
+            gl.delete_shader(Some(raw));
+        }
+    }
+
+    impl GlResource for WebGlVertexArrayObject {
+        const LABEL: &'static str = "vertex array";
+
+        fn create(gl: &WebGl2RenderingContext) -> Option<Self> {
+            gl.create_vertex_array()
+        }
+
+        fn delete(gl: &WebGl2RenderingContext, raw: &Self) {
+            gl.delete_vertex_array(Some(raw));
+        }
+    }
+
+    pub(super) type Texture = Resource<WebGlTexture>;
+    pub(super) type Buffer = Resource<WebGlBuffer>;
+    pub(super) type Framebuffer = Resource<WebGlFramebuffer>;
+    pub(super) type Program = Resource<WebGlProgram>;
+    pub(super) type VertexShader = Resource<RawVertexShader>;
+    pub(super) type FragmentShader = Resource<RawFragmentShader>;
+    pub(super) type VertexArray = Resource<WebGlVertexArrayObject>;
+}