mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-03 02:21:05 +08:00
Improve rendering performance in the Computed Styles pane
Currently the Computed Styles pane renders several hundreds of computed styles in a single cycle, which means creating, appending, layout-out, rendering, and painting hundreds of heavily styled <li> items that contains widgets and children. This can create a visual lag ranging from ~100ms to ~1s. This CL breaks down these operations into smaller batches, and queues them using browser's requestIdleCallback to provide an instant rendering feedback. tracing before: https://chromedevtools.github.io/timeline-viewer/?loadTimelineFromURL=https://gist.githubusercontent.com/hanselfmu/424153ea140df049800e716031882238/raw/c62f1561cf4f56c1c5c869325015f3d99ab65637/computed_tab_perf_before.json tracing after: https://chromedevtools.github.io/timeline-viewer/?loadTimelineFromURL=https://gist.github.com/hanselfmu/5fdc43d3ae878aed82e79ff3af3c1ed6/raw/7d5095dfde0236eccafe53ec0fb22356e303afb9/computed_tab_perf_after.json Bug: chromium:1100148, chromium:1106251, chromium:1093263 Change-Id: Id0acbf91542280b026fdb67e28097bdb445606ea Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2308547 Commit-Queue: Changhao Han <changhaohan@chromium.org> Reviewed-by: Alex Rudenko <alexrudenko@chromium.org> Reviewed-by: Mathias Bynens <mathias@chromium.org>
This commit is contained in:
@@ -36,7 +36,7 @@ import * as UI from '../ui/ui.js';
|
||||
import {ComputedStyle, ComputedStyleModel, Events} from './ComputedStyleModel.js'; // eslint-disable-line no-unused-vars
|
||||
import {ImagePreviewPopover} from './ImagePreviewPopover.js';
|
||||
import {PlatformFontsWidget} from './PlatformFontsWidget.js';
|
||||
import {StylePropertiesSection, StylesSidebarPane, StylesSidebarPropertyRenderer} from './StylesSidebarPane.js';
|
||||
import {IdleCallbackManager, StylePropertiesSection, StylesSidebarPane, StylesSidebarPropertyRenderer} from './StylesSidebarPane.js';
|
||||
|
||||
/**
|
||||
* @param {!SDK.DOMModel.DOMNode} node
|
||||
@@ -215,6 +215,9 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget {
|
||||
|
||||
const fontsWidget = new PlatformFontsWidget(this._computedStyleModel);
|
||||
fontsWidget.show(this.contentElement);
|
||||
|
||||
/** @type {?IdleCallbackManager} */
|
||||
this._idleCallbackManager = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,6 +232,17 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget {
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
update() {
|
||||
if (this._idleCallbackManager) {
|
||||
this._idleCallbackManager.discard();
|
||||
}
|
||||
this._idleCallbackManager = new IdleCallbackManager();
|
||||
super.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @return {!Promise.<?>}
|
||||
@@ -236,7 +250,7 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget {
|
||||
async doUpdate() {
|
||||
const promises = [this._computedStyleModel.fetchComputedStyle(), this._fetchMatchedCascade()];
|
||||
const [nodeStyles, matchedStyles] = await Promise.all(promises);
|
||||
this._innerRebuildUpdate(nodeStyles, matchedStyles);
|
||||
await this._innerRebuildUpdate(nodeStyles, matchedStyles);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,7 +278,7 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget {
|
||||
* @param {?ComputedStyle} nodeStyle
|
||||
* @param {?SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles
|
||||
*/
|
||||
_innerRebuildUpdate(nodeStyle, matchedStyles) {
|
||||
async _innerRebuildUpdate(nodeStyle, matchedStyles) {
|
||||
/** @type {!Set<string>} */
|
||||
const expandedProperties = new Set();
|
||||
for (const treeElement of this._propertiesOutline.rootElement().children()) {
|
||||
@@ -287,11 +301,13 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget {
|
||||
const uniqueProperties = [...nodeStyle.computedStyle.keys()];
|
||||
uniqueProperties.sort(propertySorter);
|
||||
|
||||
const node = nodeStyle.node;
|
||||
const propertyTraces = this._computePropertyTraces(matchedStyles);
|
||||
const inheritedProperties = this._computeInheritedProperties(matchedStyles);
|
||||
const showInherited = this._showInheritedComputedStylePropertiesSetting.get();
|
||||
for (let i = 0; i < uniqueProperties.length; ++i) {
|
||||
const propertyName = uniqueProperties[i];
|
||||
const computedStyleQueue = [];
|
||||
// filter and preprocess properties to line up in the computed style queue
|
||||
for (const propertyName of uniqueProperties) {
|
||||
const propertyValue = nodeStyle.computedStyle.get(propertyName);
|
||||
const canonicalName = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propertyName);
|
||||
const isInherited = !inheritedProperties.has(canonicalName);
|
||||
@@ -304,38 +320,62 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget {
|
||||
if (propertyName !== canonicalName && propertyValue === nodeStyle.computedStyle.get(canonicalName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const {propertyElement, propertyValueElement} =
|
||||
createPropertyElement(nodeStyle.node, propertyName, propertyValue, isInherited);
|
||||
|
||||
const treeElement = new UI.TreeOutline.TreeElement();
|
||||
treeElement.title = propertyElement;
|
||||
treeElement[_propertySymbol] = {name: propertyName, value: propertyValue};
|
||||
const isOdd = this._propertiesOutline.rootElement().children().length % 2 === 0;
|
||||
treeElement.listItemElement.classList.toggle('odd-row', isOdd);
|
||||
this._propertiesOutline.appendChild(treeElement);
|
||||
if (!this._propertiesOutline.selectedTreeElement) {
|
||||
treeElement.select(!hadFocus);
|
||||
}
|
||||
|
||||
const trace = propertyTraces.get(propertyName);
|
||||
if (trace) {
|
||||
const activeProperty = this._renderPropertyTrace(matchedStyles, nodeStyle.node, treeElement, trace);
|
||||
treeElement.listItemElement.addEventListener('mousedown', e => e.consume(), false);
|
||||
treeElement.listItemElement.addEventListener('dblclick', e => e.consume(), false);
|
||||
treeElement.listItemElement.addEventListener('click', handleClick.bind(null, treeElement), false);
|
||||
treeElement.listItemElement.addEventListener(
|
||||
'contextmenu', this._handleContextMenuEvent.bind(this, matchedStyles, activeProperty));
|
||||
const gotoSourceElement = UI.Icon.Icon.create('mediumicon-arrow-in-circle', 'goto-source-icon');
|
||||
gotoSourceElement.addEventListener('click', navigateToSource.bind(this, activeProperty));
|
||||
propertyValueElement.appendChild(gotoSourceElement);
|
||||
if (expandedProperties.has(propertyName)) {
|
||||
treeElement.expand();
|
||||
}
|
||||
}
|
||||
computedStyleQueue.push({propertyName, propertyValue, isInherited});
|
||||
}
|
||||
|
||||
this._updateFilter(this._filterRegex);
|
||||
// Render computed style properties in batches via idle callbacks to avoid a
|
||||
// very long task. The batchSize and timeoutInterval should be tweaked in
|
||||
// pair. Currently, updating, laying-out, rendering, and painting 20 items
|
||||
// in every 100ms seems to be a good balance between updating too lazy vs.
|
||||
// updating too much in one cycle.
|
||||
const batchSize = 20;
|
||||
const timeoutInterval = 100;
|
||||
let timeout = 100;
|
||||
while (computedStyleQueue.length > 0) {
|
||||
const currentBatch = computedStyleQueue.splice(0, batchSize);
|
||||
|
||||
this._idleCallbackManager.schedule(() => {
|
||||
for (const {propertyName, propertyValue, isInherited} of currentBatch) {
|
||||
const {propertyElement, propertyValueElement} =
|
||||
createPropertyElement(node, propertyName, propertyValue, isInherited);
|
||||
const treeElement = new UI.TreeOutline.TreeElement();
|
||||
treeElement.title = propertyElement;
|
||||
treeElement[_propertySymbol] = {name: propertyName, value: propertyValue};
|
||||
if (!this._propertiesOutline.selectedTreeElement) {
|
||||
treeElement.select(!hadFocus);
|
||||
}
|
||||
|
||||
const trace = propertyTraces.get(propertyName);
|
||||
if (trace) {
|
||||
const activeProperty = this._renderPropertyTrace(
|
||||
/** @type {!SDK.CSSMatchedStyles.CSSMatchedStyles} */ (matchedStyles), node, treeElement, trace);
|
||||
treeElement.listItemElement.addEventListener('mousedown', e => e.consume(), false);
|
||||
treeElement.listItemElement.addEventListener('dblclick', e => e.consume(), false);
|
||||
treeElement.listItemElement.addEventListener('click', handleClick.bind(null, treeElement), false);
|
||||
treeElement.listItemElement.addEventListener(
|
||||
'contextmenu',
|
||||
this._handleContextMenuEvent.bind(
|
||||
this, /** @type {!SDK.CSSMatchedStyles.CSSMatchedStyles} */ (matchedStyles), activeProperty));
|
||||
const gotoSourceElement = UI.Icon.Icon.create('mediumicon-arrow-in-circle', 'goto-source-icon');
|
||||
gotoSourceElement.addEventListener('click', navigateToSource.bind(this, activeProperty));
|
||||
propertyValueElement.appendChild(gotoSourceElement);
|
||||
if (expandedProperties.has(propertyName)) {
|
||||
treeElement.expand();
|
||||
}
|
||||
}
|
||||
|
||||
this._propertiesOutline.appendChild(treeElement);
|
||||
const isEven = this._propertiesOutline.rootElement().children().length % 2 === 0;
|
||||
treeElement.listItemElement.classList.toggle('even-row', isEven);
|
||||
}
|
||||
|
||||
this._updateFilter(this._filterRegex);
|
||||
}, timeout);
|
||||
|
||||
timeout += timeoutInterval;
|
||||
}
|
||||
|
||||
await this._idleCallbackManager.awaitDone();
|
||||
|
||||
/**
|
||||
* @param {string} a
|
||||
|
||||
@@ -988,7 +988,7 @@ export class SectionBlock {
|
||||
}
|
||||
}
|
||||
|
||||
class IdleCallbackManager {
|
||||
export class IdleCallbackManager {
|
||||
constructor() {
|
||||
this._discarded = false;
|
||||
/** @type {!Array<!Promise<void>>} */
|
||||
@@ -1001,8 +1001,9 @@ class IdleCallbackManager {
|
||||
|
||||
/**
|
||||
* @param {function():void} fn
|
||||
* @param {number} timeout
|
||||
*/
|
||||
schedule(fn) {
|
||||
schedule(fn, timeout = 100) {
|
||||
if (this._discarded) {
|
||||
return;
|
||||
}
|
||||
@@ -1020,7 +1021,7 @@ class IdleCallbackManager {
|
||||
return resolve();
|
||||
}
|
||||
run();
|
||||
}, {timeout: 100});
|
||||
}, {timeout});
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tree-outline li.odd-row {
|
||||
.tree-outline li.even-row {
|
||||
position: relative;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
@@ -364,8 +364,8 @@ ElementsTestRunner.selectNodeAndWaitForStylesWithComputed = function(idValue, ca
|
||||
callback = TestRunner.safeWrap(callback);
|
||||
ElementsTestRunner.selectNodeAndWaitForStyles(idValue, onSidebarRendered);
|
||||
|
||||
function onSidebarRendered(node) {
|
||||
ElementsTestRunner.computedStyleWidget().doUpdate().then(callback.bind(null, node));
|
||||
async function onSidebarRendered(node) {
|
||||
await ElementsTestRunner.computedStyleWidget().doUpdate().then(callback.bind(null, node));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('The Computed pane', async () => {
|
||||
let content = await getContentOfComputedPane();
|
||||
await frontend.keyboard.press('ArrowDown');
|
||||
await waitForComputedPaneChange(content);
|
||||
await waitForElementsComputedSection();
|
||||
|
||||
const h1Properties = await getAllPropertiesFromComputedPane();
|
||||
assert.strictEqual(h1Properties.length, 10, 'There should be 10 computed properties on the H1 element');
|
||||
@@ -40,6 +41,7 @@ describe('The Computed pane', async () => {
|
||||
content = await getContentOfComputedPane();
|
||||
await frontend.keyboard.press('ArrowDown');
|
||||
await waitForComputedPaneChange(content);
|
||||
await waitForElementsComputedSection();
|
||||
|
||||
const h2Properties = await getAllPropertiesFromComputedPane();
|
||||
assert.strictEqual(h2Properties.length, 11, 'There should be 11 computed properties on the H2 element');
|
||||
@@ -61,6 +63,7 @@ describe('The Computed pane', async () => {
|
||||
await waitForComputedPaneChange(content);
|
||||
|
||||
await toggleShowAllComputedProperties();
|
||||
await waitForElementsComputedSection();
|
||||
|
||||
const allH1Properties = await getAllPropertiesFromComputedPane();
|
||||
const alignContentProperty = allH1Properties.find(property => property && property.name === 'align-content');
|
||||
|
||||
Reference in New Issue
Block a user