Skip to content

Commit

Permalink
Improve UI performance by optimizing reactivity
Browse files Browse the repository at this point in the history
- Replace `ref`s with `shallowRef` when deep reactivity is not needed.
- Replace `readonly`s with `shallowReadonly` where the goal is to only
  prevent `.value` mutation.
- Remove redundant `ref` in `SizeObserver.vue`.
- Remove redundant nested `ref` in `TooltipWrapper.vue`.
- Remove redundant `events` export from `UseCollectionState.ts`.
- Remove redundant `computed` from `UseCollectionState.ts`.
- Remove `timestamp` from `TreeViewFilterEvent` that becomes unnecessary
  after using `shallowRef`.
- Add missing unit tests for `UseTreeViewFilterEvent`.
- Add missing stub for `FilterChangeDetails`.
  • Loading branch information
undergroundwires committed Oct 30, 2023
1 parent 77123d8 commit ee02bb3
Show file tree
Hide file tree
Showing 24 changed files with 377 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import { defineComponent, shallowRef } from 'vue';
import SliderHandle from './SliderHandle.vue';
export default defineComponent({
Expand All @@ -45,7 +45,7 @@ export default defineComponent({
},
},
setup() {
const firstElement = ref<HTMLElement>();
const firstElement = shallowRef<HTMLElement>();
function onResize(displacementX: number): void {
const leftWidth = firstElement.value.offsetWidth + displacementX;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<script lang="ts">
import {
defineComponent, ref, watch, computed,
inject,
inject, shallowRef,
} from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
Expand Down Expand Up @@ -95,7 +95,7 @@ export default defineComponent({
const isAnyChildSelected = ref(false);
const areAllChildrenSelected = ref(false);
const cardElement = ref<HTMLElement>();
const cardElement = shallowRef<HTMLElement>();
const cardTitle = computed<string | undefined>(() => {
if (!props.categoryId || !currentState.value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@ import type { ReadOnlyTreeNode } from '../Node/TreeNode';

export interface TreeViewFilterEvent {
readonly action: TreeViewFilterAction;
/**
* A simple numeric value to ensure uniqueness of each event.
*
* This property is used to guarantee that the watch function will trigger
* even if the same filter action value is emitted consecutively.
*/
readonly timestamp: Date;

readonly predicate?: TreeViewFilterPredicate;
}

Expand All @@ -25,14 +17,12 @@ export function createFilterTriggeredEvent(
): TreeViewFilterEvent {
return {
action: TreeViewFilterAction.Triggered,
timestamp: new Date(),
predicate,
};
}

export function createFilterRemovedEvent(): TreeViewFilterEvent {
return {
action: TreeViewFilterAction.Removed,
timestamp: new Date(),
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
WatchSource, inject, ref, watch,
WatchSource, inject, shallowRef, watch,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { ReadOnlyTreeNode } from './TreeNode';
Expand All @@ -10,7 +10,7 @@ export function useNodeState(
) {
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();

const state = ref<TreeNodeStateDescriptor>();
const state = shallowRef<TreeNodeStateDescriptor>();

watch(nodeWatcher, (node: ReadOnlyTreeNode) => {
if (!node) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<script lang="ts">
import {
defineComponent, onMounted, watch,
ref, PropType,
shallowRef, PropType,
} from 'vue';
import { TreeRootManager } from './TreeRoot/TreeRootManager';
import TreeRoot from './TreeRoot/TreeRoot.vue';
Expand Down Expand Up @@ -53,7 +53,7 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const treeContainerElement = ref<HTMLElement | undefined>();
const treeContainerElement = shallowRef<HTMLElement | undefined>();
const tree = new TreeRootManager();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
WatchSource, watch, inject, readonly, ref,
WatchSource, watch, inject, shallowReadonly, shallowRef,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { TreeRoot } from './TreeRoot/TreeRoot';
Expand All @@ -8,8 +8,8 @@ import { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes';
export function useCurrentTreeNodes(treeWatcher: WatchSource<TreeRoot>) {
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();

const tree = ref<TreeRoot | undefined>();
const nodes = ref<QueryableNodes | undefined>();
const tree = shallowRef<TreeRoot | undefined>();
const nodes = shallowRef<QueryableNodes | undefined>();

watch(treeWatcher, (newTree) => {
tree.value = newTree;
Expand All @@ -22,6 +22,6 @@ export function useCurrentTreeNodes(treeWatcher: WatchSource<TreeRoot>) {
}, { immediate: true });

return {
nodes: readonly(nodes),
nodes: shallowReadonly(nodes),
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
WatchSource, inject, watch, ref,
WatchSource, inject, watch, shallowRef,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
Expand All @@ -17,7 +17,7 @@ export function useNodeStateChangeAggregator(
const { nodes } = useTreeNodes(treeWatcher);
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();

const onNodeChangeCallback = ref<NodeStateChangeEventCallback>();
const onNodeChangeCallback = shallowRef<NodeStateChangeEventCallback>();

watch([
() => nodes.value,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
computed, inject, readonly, ref,
computed, inject, shallowReadonly, shallowRef,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
Expand All @@ -15,15 +15,15 @@ export function useSelectedScriptNodeIds(scriptNodeIdParser = getScriptNodeId) {
});

return {
selectedScriptNodeIds: readonly(selectedNodeIds),
selectedScriptNodeIds: shallowReadonly(selectedNodeIds),
};
}

function useSelectedScripts() {
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
const { onStateChange } = inject(InjectionKeys.useCollectionState)();

const selectedScripts = ref<readonly SelectedScript[]>([]);
const selectedScripts = shallowRef<readonly SelectedScript[]>([]);

onStateChange((state) => {
selectedScripts.value = state.selection.selectedScripts;
Expand All @@ -35,6 +35,6 @@ function useSelectedScripts() {
}, { immediate: true });

return {
selectedScripts: readonly(selectedScripts),
selectedScripts: shallowReadonly(selectedScripts),
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
Ref, inject, readonly, ref,
Ref, inject, shallowReadonly, shallowRef,
} from 'vue';
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
Expand All @@ -21,7 +21,7 @@ export function useTreeViewFilterEvent() {
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();

const latestFilterEvent = ref<TreeViewFilterEvent | undefined>(undefined);
const latestFilterEvent = shallowRef<TreeViewFilterEvent | undefined>(undefined);

const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches(
getNodeMetadata(node),
Expand All @@ -36,7 +36,7 @@ export function useTreeViewFilterEvent() {
}, { immediate: true });

return {
latestFilterEvent: readonly(latestFilterEvent),
latestFilterEvent: shallowReadonly(latestFilterEvent),
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {
ref, computed, readonly,
} from 'vue';
import { shallowRef, shallowReadonly } from 'vue';
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
Expand All @@ -16,7 +14,7 @@ export function useCollectionState(
throw new Error('missing events');
}

const currentState = ref<ICategoryCollectionState>(context.state);
const currentState = shallowRef<IReadOnlyCategoryCollectionState>(context.state);
events.register([
context.contextChanged.on((event) => {
currentState.value = event.newState;
Expand Down Expand Up @@ -66,8 +64,7 @@ export function useCollectionState(
modifyCurrentContext,
onStateChange,
currentContext: context as IReadOnlyApplicationContext,
currentState: readonly(computed<IReadOnlyCategoryCollectionState>(() => currentState.value)),
events: events as IEventSubscriptionCollection,
currentState: shallowReadonly(currentState),
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/presentation/components/Shared/Icon/UseSvgLoader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
WatchSource, readonly, ref, watch,
WatchSource, shallowReadonly, ref, watch,
} from 'vue';
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
import { IconName } from './IconName';
Expand All @@ -15,7 +15,7 @@ export function useSvgLoader(
}, { immediate: true });

return {
svgContent: readonly(svgContent),
svgContent: shallowReadonly(svgContent),
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/presentation/components/Shared/Modal/ModalContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import { defineComponent, shallowRef } from 'vue';
export default defineComponent({
props: {
Expand All @@ -31,7 +31,7 @@ export default defineComponent({
'transitionedOut',
],
setup(_, { emit }) {
const modalElement = ref<HTMLElement>();
const modalElement = shallowRef<HTMLElement>();
function onAfterTransitionLeave() {
emit('transitionedOut');
Expand Down
6 changes: 3 additions & 3 deletions src/presentation/components/Shared/SizeObserver.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<template>
<div ref="containerElement" class="container">
<slot ref="containerElement" />
<slot />
</div>
</template>

<script lang="ts">
import {
defineComponent, ref, onMounted, onBeforeUnmount,
defineComponent, shallowRef, onMounted, onBeforeUnmount,
} from 'vue';
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
Expand All @@ -21,7 +21,7 @@ export default defineComponent({
setup(_, { emit }) {
const { resizeObserverReady } = useResizeObserverPolyfill();
const containerElement = ref<HTMLElement>();
const containerElement = shallowRef<HTMLElement>();
let width = 0;
let height = 0;
Expand Down
12 changes: 6 additions & 6 deletions src/presentation/components/Shared/TooltipWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import {
useFloating, arrow, shift, flip, Placement, offset, Side, Coords, autoUpdate,
} from '@floating-ui/vue';
import { defineComponent, ref, computed } from 'vue';
import { defineComponent, shallowRef, computed } from 'vue';
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
import type { CSSProperties } from 'vue/types/jsx'; // In Vue 3.0 import from 'vue'
Expand All @@ -36,18 +36,18 @@ const MARGIN_FROM_DOCUMENT_EDGE_IN_PX = 2;
export default defineComponent({
setup() {
const tooltipDisplayElement = ref<HTMLElement | undefined>();
const triggeringElement = ref<HTMLElement | undefined>();
const arrowElement = ref<HTMLElement | undefined>();
const placement = ref<Placement>('top');
const tooltipDisplayElement = shallowRef<HTMLElement | undefined>();
const triggeringElement = shallowRef<HTMLElement | undefined>();
const arrowElement = shallowRef<HTMLElement | undefined>();
const placement = shallowRef<Placement>('top');
useResizeObserverPolyfill();
const { floatingStyles, middlewareData } = useFloating(
triggeringElement,
tooltipDisplayElement,
{
placement: ref(placement),
placement,
middleware: [
offset(ARROW_SIZE_IN_PX + GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX),
/* Shifts the element along the specified axes in order to keep it in view. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
describe, it, expect,
} from 'vitest';
import { mount } from '@vue/test-utils';
import { defineComponent, ref } from 'vue';
import { defineComponent, shallowRef } from 'vue';
import TreeView from '@/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue';
import { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
import { provideDependencies } from '@/presentation/bootstrapping/DependencyProvider';
Expand Down Expand Up @@ -33,8 +33,8 @@ function createTreeViewWrapper(initialNodeData: readonly TreeInputNodeData[]) {
setup() {
provideDependencies(new ApplicationContextStub());

const initialNodes = ref(initialNodeData);
const selectedLeafNodeIds = ref<readonly string[]>([]);
const initialNodes = shallowRef(initialNodeData);
const selectedLeafNodeIds = shallowRef<readonly string[]>([]);
return {
initialNodes,
selectedLeafNodeIds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
import { UserFilter } from '@/application/Context/State/Filter/UserFilter';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsStub';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { FilterChange } from '@/application/Context/State/Filter/Event/FilterChange';
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
import { ICategoryCollection } from '@/domain/ICategoryCollection';

describe('UserFilter', () => {
describe('clearFilter', () => {
it('signals when removing filter', () => {
// arrange
const expectedChange = FilterChange.forClear();
const expectedChange = FilterChangeDetailsStub.forClear();
let actualChange: IFilterChangeDetails;
const sut = new UserFilter(new CategoryCollectionStub());
sut.filterChanged.on((change) => {
Expand Down
Loading

0 comments on commit ee02bb3

Please sign in to comment.