Performance optimization on a large webapp

EXPERIENCE OF DEVELOPING WEWEB EDITOR

WEWEB

PHOTO GATHER

WEWEB

DISCLAIMER

TOOLS

app.config.performance = true

Configure Vue

Browser devtools

Vuejs Devtool

TOOLS

export default {
  renderTracked(event) {
    debugger
  },
  renderTriggered(event) {
    debugger
  }
}
const property = computed(() => getValue(key), {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})
watch(source, callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

Demo time

https://performance.purple-fox.fr

https://github.com/AurelieV/opti-perf

It's not always VUEJS

REFLOW PROBLEM DEMO

calcMenuPosition() {
  if (!this.isSelected) return;
  const { height, width } = this.$refs.el.getBoundingClientRect();
  this.positionStyle = {
    height: `${height}px`,
    width: `${width}px`,
  };
  requestAnimationFrame(() => this.calcMenuPosition());
},

REFLOW PROBLEM DEMO

watch: {
  isSelected(value) {
    if (value) {
      if (!this.observer) {
        this.observer = new ResizeObserver((entries) => {
          for (const entry of entries) {
            const rect = entry.contentRect;
            this.positionStyle = {
              height: `${rect.height}px`,
              width: `${rect.width}px`,
            };
          }
        });
      }
      this.observer.observe(this.$refs.el);
    } else {
      if (this.observer) {
        this.observer.disconnect();
      }
    }
  },
},

OTHER COMMON PROBLEMS

CSS SELECTOR

CSS (height: 100%)

Scroll position

TRACK reactivity

export const collections = computed(() => {
  const collections = wwLib.$store.getters['data/getCollections'];
  return Object.keys(collections).reduce((obj, key) => {
    const item = collections[key]
    obj[item.name] = item;
    obj[key] = item;
    return obj;
  }, {})
})
export const collections = computed(() => {
  const collections = wwLib.$store.getters['data/getCollections'];
  return Object.keys(collections).reduce((obj, key) => {
    const item = {
      id: collections[key].id,
      data: collections[key].data,
      isFetched: collections[key].isFetched,
      isFetching: collections[key].isFetching
    }
    obj[item.name] = item;
    obj[key] = item;
    return obj;
  }, {})
})

DEMO

Code 1: Only track the keys of collections object

Code 2: Track all properties of each item

COMPUTED 

const isSelected = computed(() => props.id === store.selectedId)

COMPUTED 

const isSelected = computed(() => props.id === store.selectedId)

SOLUTIONS

CHILD COMPONENT

v-MEMO

"LAZY" REF

V-MEMO

<ul class="grid grid-cols-4 gap-3">
  <li
    class="px-2 transition transform"
    v-for="i of list"
    :key="i"
    v-memo="[selectedId === i]"
    :class="{ 'rotate-3': selectedId === i }"
    @click="selectedId = i"
  ></li>
</ul>

"lazy" ref

import { ref, watch } from "vue";
import { store } from "./store";

export function useLazyRef(id) {
    const isSelected = ref(false);
    watch(
        () => store.selectedId,
        (selectedId) => {
            const newValue = selectedId === id;
            if (isSelected !== newValue) {
                isSelected.value = newValue;
            }
        },
        { immediate: true }
    );

    return isSelected;
}

effect scope

Effect?

Effect scope

shared effect

function createSharedRef(createRef) {
    let value = null;
    let scope = null;
    let subscribers = 0;

    function dispose() {
        subscribers--;
        if (scope && subscribers <= 0) {
            scope.stop();
            value = null;
            scope = null;
        }
    }

    return (...args) => {
        subscribers++;
        if (!value) {
            scope = effectScope(true);
            value = scope.run(() => createRef(...args));
        }
        onScopeDispose(dispose);
        return value;
    };
}

export const useSharedDataProperty = createSharedRef(useDataProperty);

"activable" effect

function useDistance(id, isActive) {
  let scope;
  const distance = ref(0);

  function dispose() {
    if (scope) {
      scope.stop();
      scope = null;
    }
  }
  
  watch(isActive, isActive => {
    if (!isActive) {
      dispose();
    } else {
      scope = effectScope();
      scope.run(() => {
        watch(
          params,
          params => distance.value = computeDistance(id, params),
          { immediate: true }
        )
      })
  })
 
  onScopeDispose(dispose);
  
  return distance;
}

stay IN TOUCH

@purple_orwel

Enjoy the pictures? Contact Emmanuelle here

Made with Slides.com