Skip to content

Commit

Permalink
Merge pull request #602 from capricorn86/task/599-massive-performance…
Browse files Browse the repository at this point in the history
…-downgrade-from-v700

#599@minor: Improves performance of window.getComputedStyle() by addi…
  • Loading branch information
capricorn86 committed Oct 10, 2022
2 parents fb96afe + bd300f1 commit 7265c36
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 86 deletions.
Expand Up @@ -86,6 +86,10 @@ export default abstract class AbstractCSSStyleDeclaration {
this._ownerElement['_attributes']['style'].name = 'style';
}

if (this._ownerElement.isConnected) {
this._ownerElement.ownerDocument['_cacheID']++;
}

this._ownerElement['_attributes']['style'].value = style.toString();
}
} else {
Expand Down Expand Up @@ -137,6 +141,10 @@ export default abstract class AbstractCSSStyleDeclaration {
const style = this._elementStyle.getElementStyle();
style.set(name, value, !!priority);

if (this._ownerElement.isConnected) {
this._ownerElement.ownerDocument['_cacheID']++;
}

this._ownerElement['_attributes']['style'].value = style.toString();
} else {
this._style.set(name, value, !!priority);
Expand All @@ -163,6 +171,10 @@ export default abstract class AbstractCSSStyleDeclaration {
style.remove(name);
const newCSSText = style.toString();
if (newCSSText) {
if (this._ownerElement.isConnected) {
this._ownerElement.ownerDocument['_cacheID']++;
}

this._ownerElement['_attributes']['style'].value = newCSSText;
} else {
delete this._ownerElement['_attributes']['style'];
Expand Down
Expand Up @@ -12,14 +12,29 @@ import CSSStyleRule from '../../rules/CSSStyleRule';
import CSSStyleDeclarationElementDefaultCSS from './CSSStyleDeclarationElementDefaultCSS';
import CSSStyleDeclarationElementInheritedProperties from './CSSStyleDeclarationElementInheritedProperties';
import CSSStyleDeclarationCSSParser from './CSSStyleDeclarationCSSParser';
import QuerySelector from '../../../query-selector/QuerySelector';

const CSS_VARIABLE_REGEXP = /var\( *(--[^) ]+)\)/g;

type IStyleAndElement = {
element: IElement | IShadowRoot | IDocument;
cssTexts: Array<{ cssText: string; priorityWeight: number }>;
};

/**
* CSS Style Declaration utility
*/
export default class CSSStyleDeclarationElementStyle {
private cache: { [k: string]: CSSStyleDeclarationPropertyManager } = {};
private cache: {
propertyManager: CSSStyleDeclarationPropertyManager;
cssText: string;
documentCacheID: number;
} = {
propertyManager: null,
cssText: null,
documentCacheID: null
};

private element: IElement;
private computed: boolean;

Expand Down Expand Up @@ -47,11 +62,12 @@ export default class CSSStyleDeclarationElementStyle {
const cssText = this.element['_attributes']['style']?.value;

if (cssText) {
if (this.cache[cssText]) {
return this.cache[cssText];
if (this.cache.propertyManager && this.cache.cssText === cssText) {
return this.cache.propertyManager;
}
this.cache[cssText] = new CSSStyleDeclarationPropertyManager({ cssText });
return this.cache[cssText];
this.cache.cssText = cssText;
this.cache.propertyManager = new CSSStyleDeclarationPropertyManager({ cssText });
return this.cache.propertyManager;
}

return new CSSStyleDeclarationPropertyManager();
Expand All @@ -64,28 +80,37 @@ export default class CSSStyleDeclarationElementStyle {
* @returns Style sheets.
*/
private getComputedElementStyle(): CSSStyleDeclarationPropertyManager {
const documentElements: Array<{ element: IElement; cssText: string }> = [];
const parentElements: Array<{ element: IElement; cssText: string }> = [];
let styleAndElement = {
const documentElements: Array<IStyleAndElement> = [];
const parentElements: Array<IStyleAndElement> = [];
let styleAndElement: IStyleAndElement = {
element: <IElement | IShadowRoot | IDocument>this.element,
cssText: ''
cssTexts: []
};
let shadowRootElements: Array<{ element: IElement; cssText: string }> = [];
let shadowRootElements: Array<IStyleAndElement> = [];

if (!this.element.isConnected) {
return new CSSStyleDeclarationPropertyManager();
}

if (
this.cache.propertyManager &&
this.cache.documentCacheID === this.element.ownerDocument['_cacheID']
) {
return this.cache.propertyManager;
}

this.cache.documentCacheID = this.element.ownerDocument['_cacheID'];

// Walks through all parent elements and stores them in an array with element and matching CSS text.
while (styleAndElement.element) {
if (styleAndElement.element.nodeType === NodeTypeEnum.elementNode) {
const rootNode = styleAndElement.element.getRootNode();
if (rootNode.nodeType === NodeTypeEnum.documentNode) {
documentElements.unshift(<{ element: IElement; cssText: string }>styleAndElement);
documentElements.unshift(styleAndElement);
} else {
shadowRootElements.unshift(<{ element: IElement; cssText: string }>styleAndElement);
shadowRootElements.unshift(styleAndElement);
}
parentElements.unshift(<{ element: IElement; cssText: string }>styleAndElement);
parentElements.unshift(styleAndElement);
}

if (styleAndElement.element === this.element.ownerDocument) {
Expand All @@ -103,15 +128,15 @@ export default class CSSStyleDeclarationElementStyle {
}
}

styleAndElement = { element: null, cssText: '' };
styleAndElement = { element: null, cssTexts: [] };
} else if ((<IShadowRoot>styleAndElement.element).host) {
const styleSheets = <INodeList<IHTMLStyleElement>>(
(<IShadowRoot>styleAndElement.element).querySelectorAll('style,link[rel="stylesheet"]')
);

styleAndElement = {
element: <IElement>(<IShadowRoot>styleAndElement.element).host,
cssText: ''
cssTexts: []
};

for (const styleSheet of styleSheets) {
Expand All @@ -120,39 +145,58 @@ export default class CSSStyleDeclarationElementStyle {
this.parseCSSRules({
elements: shadowRootElements,
cssRules: sheet.cssRules,
hostElement: <{ element: IElement; cssText: string }>styleAndElement
hostElement: styleAndElement
});
}
}
shadowRootElements = [];
} else {
styleAndElement = { element: <IElement>styleAndElement.element.parentNode, cssText: '' };
styleAndElement = { element: <IElement>styleAndElement.element.parentNode, cssTexts: [] };
}
}

// Concatenates all parent element CSS to one string.
const targetElement = parentElements[parentElements.length - 1];
let inheritedCSSText = CSSStyleDeclarationElementDefaultCSS.default;
let inheritedCSSText = '';

for (const parentElement of parentElements) {
if (parentElement !== targetElement) {
inheritedCSSText +=
(CSSStyleDeclarationElementDefaultCSS[parentElement.element.tagName] || '') +
parentElement.cssText +
(parentElement.element['_attributes']['style']?.value || '');
parentElement.cssTexts.sort((a, b) => a.priorityWeight - b.priorityWeight);

if (CSSStyleDeclarationElementDefaultCSS[(<IElement>parentElement.element).tagName]) {
inheritedCSSText +=
CSSStyleDeclarationElementDefaultCSS[(<IElement>parentElement.element).tagName];
}

for (const cssText of parentElement.cssTexts) {
inheritedCSSText += cssText.cssText;
}

if (parentElement.element['_attributes']['style']?.value) {
inheritedCSSText += parentElement.element['_attributes']['style'].value;
}
}
}

const cssVariables: { [k: string]: string } = {};
const properties = {};
const targetCSSText =
(CSSStyleDeclarationElementDefaultCSS[targetElement.element.tagName] || '') +
targetElement.cssText +
(targetElement.element['_attributes']['style']?.value || '');
let targetCSSText =
CSSStyleDeclarationElementDefaultCSS[(<IElement>targetElement.element).tagName] || '';

targetElement.cssTexts.sort((a, b) => a.priorityWeight - b.priorityWeight);

for (const cssText of targetElement.cssTexts) {
targetCSSText += cssText.cssText;
}

if (targetElement.element['_attributes']['style']?.value) {
targetCSSText += targetElement.element['_attributes']['style'].value;
}

const combinedCSSText = inheritedCSSText + targetCSSText;

if (this.cache[combinedCSSText]) {
return this.cache[combinedCSSText];
if (this.cache.propertyManager && this.cache.cssText === combinedCSSText) {
return this.cache.propertyManager;
}

// Parses the parent element CSS and stores CSS variables and inherited properties.
Expand Down Expand Up @@ -204,7 +248,8 @@ export default class CSSStyleDeclarationElementStyle {
propertyManager.set(name, properties[name].value, properties[name].important);
}

this.cache[combinedCSSText] = propertyManager;
this.cache.cssText = combinedCSSText;
this.cache.propertyManager = propertyManager;

return propertyManager;
}
Expand All @@ -216,13 +261,11 @@ export default class CSSStyleDeclarationElementStyle {
* @param options.elements Elements.
* @param options.cssRules CSS rules.
* @param [options.hostElement] Host element.
* @param [options.hostElement.element] Element.
* @param [options.hostElement.cssText] CSS text.
*/
private parseCSSRules(options: {
cssRules: CSSRule[];
elements: Array<{ element: IElement; cssText: string }>;
hostElement?: { element: IElement; cssText: string };
elements: Array<IStyleAndElement>;
hostElement?: IStyleAndElement;
}): void {
if (!options.elements.length) {
return;
Expand All @@ -236,12 +279,19 @@ export default class CSSStyleDeclarationElementStyle {
if (selectorText) {
if (selectorText.startsWith(':host')) {
if (options.hostElement) {
options.hostElement.cssText += (<CSSStyleRule>rule)._cssText;
options.hostElement.cssTexts.push({
cssText: (<CSSStyleRule>rule)._cssText,
priorityWeight: 0
});
}
} else {
for (const element of options.elements) {
if (element.element.matches(selectorText)) {
element.cssText += (<CSSStyleRule>rule)._cssText;
const matchResult = QuerySelector.match(element.element, selectorText);
if (matchResult.matches) {
element.cssTexts.push({
cssText: (<CSSStyleRule>rule)._cssText,
priorityWeight: matchResult.priorityWeight
});
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/happy-dom/src/nodes/character-data/CharacterData.ts
Expand Up @@ -56,6 +56,10 @@ export default abstract class CharacterData extends Node implements ICharacterDa
const oldValue = this._data;
this._data = data;

if (this.isConnected) {
this.ownerDocument['_cacheID']++;
}

// MutationObserver
if (this._observers.length > 0) {
for (const observer of this._observers) {
Expand Down
4 changes: 4 additions & 0 deletions packages/happy-dom/src/nodes/document/Document.ts
Expand Up @@ -60,6 +60,10 @@ export default class Document extends Node implements IDocument {
public readonly defaultView: IWindow;
public readonly _readyStateManager: DocumentReadyStateManager;
public _activeElement: IHTMLElement = null;

// Used as an unique identifier which is updated whenever the DOM gets modified.
public _cacheID = 0;

protected _isFirstWrite = true;
protected _isFirstWriteAfterOpen = false;
private _cookie = '';
Expand Down
16 changes: 9 additions & 7 deletions packages/happy-dom/src/nodes/element/Element.ts
Expand Up @@ -5,7 +5,6 @@ import DOMRect from './DOMRect';
import DOMTokenList from '../../dom-token-list/DOMTokenList';
import IDOMTokenList from '../../dom-token-list/IDOMTokenList';
import QuerySelector from '../../query-selector/QuerySelector';
import SelectorItem from '../../query-selector/SelectorItem';
import MutationRecord from '../../mutation-observer/MutationRecord';
import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum';
import NamespaceURI from '../../config/NamespaceURI';
Expand Down Expand Up @@ -744,12 +743,7 @@ export default class Element extends Node implements IElement {
* @returns "true" if matching.
*/
public matches(selector: string): boolean {
for (const part of selector.split(',')) {
if (new SelectorItem(part.trim()).match(this)) {
return true;
}
}
return false;
return QuerySelector.match(this, selector).matches;
}

/**
Expand Down Expand Up @@ -852,6 +846,10 @@ export default class Element extends Node implements IElement {
(<IElement>attribute.ownerElement) = <IElement>this;
(<IDocument>attribute.ownerDocument) = this.ownerDocument;

if (this.isConnected) {
this.ownerDocument['_cacheID']++;
}

this._attributes[name] = attribute;

this._updateDomListIndices();
Expand Down Expand Up @@ -937,6 +935,10 @@ export default class Element extends Node implements IElement {
public removeAttributeNode(attribute: IAttr): void {
delete this._attributes[attribute.name];

if (this.isConnected) {
this.ownerDocument['_cacheID']++;
}

this._updateDomListIndices();

if (
Expand Down
12 changes: 12 additions & 0 deletions packages/happy-dom/src/nodes/node/Node.ts
Expand Up @@ -308,6 +308,10 @@ export default class Node extends EventTarget implements INode {
}
}

if (this.isConnected) {
(this.ownerDocument || this)['_cacheID']++;
}

this.childNodes.push(node);

(<Node>node)._connectToNode(this);
Expand Down Expand Up @@ -345,6 +349,10 @@ export default class Node extends EventTarget implements INode {
throw new DOMException('Failed to remove node. Node is not child of parent.');
}

if (this.isConnected) {
(this.ownerDocument || this)['_cacheID']++;
}

this.childNodes.splice(index, 1);

(<Node>node)._connectToNode(null);
Expand Down Expand Up @@ -404,6 +412,10 @@ export default class Node extends EventTarget implements INode {
);
}

if (this.isConnected) {
(this.ownerDocument || this)['_cacheID']++;
}

if (newNode.parentNode) {
const index = newNode.parentNode.childNodes.indexOf(newNode);
if (index !== -1) {
Expand Down

0 comments on commit 7265c36

Please sign in to comment.