diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32ed554..4845c53 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -74,6 +74,8 @@ If you've been extending ThreeElement in your own code, or hacking on the codeba
- **Breaking Change:** Ticker events are now emitted by the three-game's `emitter`. Since we're no longer using DOM events, this means we also no longer need the `ticking` property/attribute, so it has been removed.
+- **Changed:** Instead of using a MutationObserver instance to monitor the element for updated attributes (we can't feasibly make use of `observedAttributes`, remember?), we now simply hook into `setAttribute` to react on attribute changes. This yields _significant_ (order of a magnitude) performance improvements in projects that go through element attributes a lot. Sadly, it also means that changes you're making to the DOM in your browser's dev tools will no longer be picked up automatically (but this feature may make a comeback at a later date.)
+
- **Holy crap:** `applyProps` was refactored to use `if` instead of `switch (true)`. All you Senior JavaScript Architects can finally calm down, for I am no longer impeding upon your creed!
- **Changed:** `yarn dev` now makes use of the excellent [@web/dev-server](https://modern-web.dev/docs/dev-server/overview/). This allows us to get rid of the importmap shim we had been using so far, load additional dependencies straight from our own `node_modules`, and greatly increase iteration speed during development.
diff --git a/examples/attribute-mutation-performance.html b/examples/attribute-mutation-performance.html
index 8055f4a..a617c2f 100644
--- a/examples/attribute-mutation-performance.html
+++ b/examples/attribute-mutation-performance.html
@@ -8,6 +8,16 @@
/>
+
+
@@ -29,14 +39,14 @@
color="#f30"
>
-
+
- ${Swarm(100)}
+ ${Swarm(1000)}
diff --git a/examples/reusing-resources.html b/examples/reusing-resources.html
index 699d290..7241855 100644
--- a/examples/reusing-resources.html
+++ b/examples/reusing-resources.html
@@ -16,7 +16,12 @@
-
+
diff --git a/src/BaseElement.ts b/src/BaseElement.ts
index b81f912..c7acc17 100644
--- a/src/BaseElement.ts
+++ b/src/BaseElement.ts
@@ -2,7 +2,6 @@ import * as THREE from "three"
import { ThreeGame, TickerFunction } from "./elements/three-game"
import { ThreeScene } from "./elements/three-scene"
import { IConstructable } from "./types"
-import { observeAttributeChange } from "./util/observeAttributeChange"
/**
* The `BaseElement` class extends the built-in HTMLElement class with a bit of convenience
@@ -132,8 +131,16 @@ export class BaseElement extends HTMLElement {
this.requestFrame = this.requestFrame.bind(this)
}
- /** This element's MutationObserver. */
- private _observer?: MutationObserver
+ /**
+ * We're overloading setAttribute so it also invokes attributeChangedCallback. We
+ * do this because we can't realistically make use of observedAttributes (since we don't
+ * know at the time element classes are defined what properties their wrapped objects
+ * are exposing.)
+ */
+ setAttribute(name: string, value: string) {
+ this.attributeChangedCallback(name, this.getAttribute(name)!, value)
+ super.setAttribute(name, value)
+ }
/**
* This callback is invoked when the element is deemed properly initialized. Most
@@ -152,16 +159,6 @@ export class BaseElement extends HTMLElement {
connectedCallback() {
this.debug("connectedCallback")
- /*
- When one of this element's attributes changes, apply it to the object. Custom Elements have a built-in
- mechanism for this (attributeChangedCallback and observedAttributes, but unfortunately we can't use it,
- since we don't know the set of attributes the wrapped Three.js classes expose beforehand. So instead
- we're hacking our way around it using a mutation observer. Fun times!)
- */
- this._observer ||= observeAttributeChange(this, (prop, value) => {
- this.attributeChangedCallback(prop, (this as any)[prop], value)
- })
-
/* Emit connected event */
this.dispatchEvent(new CustomEvent("connected", { bubbles: true, cancelable: false }))
@@ -217,10 +214,6 @@ export class BaseElement extends HTMLElement {
/* Invoke removedCallback */
this.removedCallback()
-
- /* Disconnect observer */
- this._observer?.disconnect()
- this._observer = undefined
})
}
}
diff --git a/test/args.test.ts b/test/args.test.ts
index b765feb..8f7daf8 100644
--- a/test/args.test.ts
+++ b/test/args.test.ts
@@ -1,4 +1,4 @@
-import { expect, fixture, html, nextFrame } from "@open-wc/testing"
+import { expect, fixture, html } from "@open-wc/testing"
import "../src"
import { ThreeElement } from "../src/ThreeElement"
@@ -17,9 +17,6 @@ describe("the args attribute", () => {
it("provides the arguments for the Three.js constructor", async () => {
const game = await render()
const fog = game.querySelector("three-fog") as ThreeElement
-
- await nextFrame()
-
expect(fog.object.color.getHexString()).to.equal("333333")
})
})
diff --git a/test/three-element.test.ts b/test/three-element.test.ts
index 6df2159..b24331e 100644
--- a/test/three-element.test.ts
+++ b/test/three-element.test.ts
@@ -1,4 +1,4 @@
-import { expect, html, nextFrame } from "@open-wc/testing"
+import { expect, html } from "@open-wc/testing"
import * as THREE from "three"
import "../src"
import { ThreeElement } from "../src/ThreeElement"
@@ -26,12 +26,9 @@ describe(" powered by ThreeElement", () => {
describe("assigning to an attribute", () => {
it("sets the wrapped object's property of the same name", async () => {
const el = await renderMeshElement()
-
expect(el.object.name).to.equal("")
el.setAttribute("name", "A good mesh")
- await nextFrame()
-
expect(el.object.name).to.equal("A good mesh")
})
@@ -41,8 +38,6 @@ describe(" powered by ThreeElement", () => {
expect(el.object.position.x).to.equal(0)
el.setAttribute("position.x", "1")
- await nextFrame()
-
expect(el.object.position.x).to.equal(1)
})
@@ -53,8 +48,6 @@ describe(" powered by ThreeElement", () => {
expect(el.object.position.x).to.equal(0)
el.setAttribute("position:x", "1")
- await nextFrame()
-
expect(el.object.position.x).to.equal(1)
})
@@ -67,8 +60,6 @@ describe(" powered by ThreeElement", () => {
expect(el.object.position.z).to.equal(0)
el.setAttribute("position", "1, 2, 3")
- await nextFrame()
-
expect(el.object.position.x).to.equal(1)
expect(el.object.position.y).to.equal(2)
expect(el.object.position.z).to.equal(3)
@@ -84,8 +75,6 @@ describe(" powered by ThreeElement", () => {
expect(el.object.scale.z).to.equal(0)
el.setAttribute("scale", "1")
- await nextFrame()
-
expect(el.object.scale.x).to.equal(1)
expect(el.object.scale.y).to.equal(1)
expect(el.object.scale.z).to.equal(1)
@@ -96,19 +85,15 @@ describe(" powered by ThreeElement", () => {
const el = await renderMeshElement()
el.setAttribute("rotation.x", "90deg")
- await nextFrame()
expect(el.object.rotation.x).to.equal(Math.PI / 2)
el.setAttribute("rotation.x", "-90deg")
- await nextFrame()
expect(el.object.rotation.x).to.equal(Math.PI / -2)
el.setAttribute("rotation.x", "0deg")
- await nextFrame()
expect(el.object.rotation.x).to.equal(0)
el.setAttribute("rotation.x", "-0.5deg")
- await nextFrame()
expect(el.object.rotation.x).to.equal((Math.PI / 180) * -0.5)
})
@@ -116,7 +101,6 @@ describe(" powered by ThreeElement", () => {
const el = await renderMeshElement()
el.setAttribute("rotation", "90deg, 0, -90deg")
- await nextFrame()
expect(el.object.rotation.x).to.equal(Math.PI / 2)
expect(el.object.rotation.y).to.equal(0)
expect(el.object.rotation.z).to.equal(Math.PI / -2)