fix(falukant): lazily render house list previews
All checks were successful
Deploy to production / deploy (push) Successful in 1m58s
All checks were successful
Deploy to production / deploy (push) Successful in 1m58s
This commit is contained in:
@@ -14,6 +14,10 @@ export default {
|
||||
props: {
|
||||
model: { type: String, required: true },
|
||||
label: { type: String, default: '' },
|
||||
// List previews must not keep a WebGL context while they are outside the
|
||||
// scroll viewport. A page with several houses otherwise exhausts the
|
||||
// browser's per-site context limit after navigating around Falukant.
|
||||
lazy: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -22,27 +26,53 @@ export default {
|
||||
renderer: null,
|
||||
renderedModel: null,
|
||||
resizeObserver: null,
|
||||
visibilityObserver: null,
|
||||
isVisible: false,
|
||||
lifecycleToken: 0,
|
||||
loadFailed: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
model() {
|
||||
this.loadModel();
|
||||
if (this.isVisible) this.loadModel();
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.init();
|
||||
await this.loadModel();
|
||||
if (!this.lazy) {
|
||||
await this.activate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.visibilityObserver = new IntersectionObserver(([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
this.activate();
|
||||
} else {
|
||||
this.deactivate();
|
||||
}
|
||||
}, { rootMargin: '120px 0px' });
|
||||
this.visibilityObserver.observe(this.$refs.container);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.dispose();
|
||||
},
|
||||
methods: {
|
||||
async activate() {
|
||||
if (this.isVisible) return;
|
||||
this.isVisible = true;
|
||||
await this.init();
|
||||
if (this.isVisible) await this.loadModel();
|
||||
},
|
||||
deactivate() {
|
||||
if (!this.isVisible) return;
|
||||
this.isVisible = false;
|
||||
this.disposeRenderer();
|
||||
},
|
||||
async init() {
|
||||
const container = this.$refs.container;
|
||||
if (!container || import.meta.env.VITE_DISABLE_3D === 'true') return;
|
||||
|
||||
const runtime = await import('@/utils/threeRuntime.js');
|
||||
if (!this.isVisible || !this.$refs.container || this.renderer) return;
|
||||
this.scene = markRaw(new runtime.Scene());
|
||||
this.scene.background = new runtime.Color(0xf7f1e6);
|
||||
this.camera = markRaw(new runtime.PerspectiveCamera(36, 1, 0.1, 100));
|
||||
@@ -63,6 +93,7 @@ export default {
|
||||
},
|
||||
async loadModel() {
|
||||
if (!this.scene || !this.renderer || !this.model) return;
|
||||
const lifecycleToken = this.lifecycleToken;
|
||||
this.loadFailed = false;
|
||||
this.removeModel();
|
||||
|
||||
@@ -78,6 +109,11 @@ export default {
|
||||
const gltf = await loader.loadAsync(`${MODEL_BASE_PATH}/${this.model}_opt.glb`);
|
||||
dracoLoader.dispose();
|
||||
|
||||
if (!this.isVisible || lifecycleToken !== this.lifecycleToken || !this.scene) {
|
||||
this.disposeObject(gltf.scene);
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderedModel = markRaw(gltf.scene);
|
||||
const box = new modelRuntime.Box3().setFromObject(this.renderedModel);
|
||||
const size = box.getSize(new modelRuntime.Vector3());
|
||||
@@ -112,15 +148,22 @@ export default {
|
||||
removeModel() {
|
||||
if (!this.renderedModel) return;
|
||||
this.scene.remove(this.renderedModel);
|
||||
this.renderedModel.traverse((object) => {
|
||||
this.disposeObject(this.renderedModel);
|
||||
this.renderedModel = null;
|
||||
},
|
||||
disposeObject(object3d) {
|
||||
object3d?.traverse((object) => {
|
||||
object.geometry?.dispose();
|
||||
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
||||
materials.filter(Boolean).forEach((material) => material.dispose());
|
||||
});
|
||||
this.renderedModel = null;
|
||||
},
|
||||
dispose() {
|
||||
disposeRenderer() {
|
||||
// Invalidates in-flight dynamic imports/model loads before they can add
|
||||
// a renderer or model after this card has left the viewport.
|
||||
this.lifecycleToken += 1;
|
||||
this.resizeObserver?.disconnect();
|
||||
this.resizeObserver = null;
|
||||
this.removeModel();
|
||||
if (this.renderer) {
|
||||
this.$refs.container?.removeChild(this.renderer.domElement);
|
||||
@@ -134,6 +177,12 @@ export default {
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
},
|
||||
dispose() {
|
||||
this.isVisible = false;
|
||||
this.visibilityObserver?.disconnect();
|
||||
this.visibilityObserver = null;
|
||||
this.disposeRenderer();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
class="house-preview"
|
||||
:model="houseModel(house.houseType)"
|
||||
:label="$t(`falukant.house.type.${house.houseType.labelTr}`)"
|
||||
lazy
|
||||
/>
|
||||
<div class="house-info">
|
||||
<h4>{{ $t(`falukant.house.type.${house.houseType.labelTr}`) }}</h4>
|
||||
|
||||
Reference in New Issue
Block a user