Optimize startup: coalesce & debounce prefs.set
Previously prefs.set broadcast many messages per each changed pref value to all open tabs, background page, popups. This lead to repeated and needless updates of various things like the toolbar icon, reapplying of styles, and whatnot. It could easily take more than 100ms on an average computer with many tabs open. Now we debounce the broadcast & sync.set and coalesce all values in one object which is then sent just once per destination.
This commit is contained in:
parent
f8d13d8dec
commit
26802e36df
9
apply.js
9
apply.js
|
@ -47,8 +47,7 @@ function applyOnMessage(request, sender, sendResponse) {
|
||||||
applyOnMessage(Object.assign(request, {styles})));
|
applyOnMessage(Object.assign(request, {styles})));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Also handle special request just for the pop-up
|
switch (request.method) {
|
||||||
switch (request.method == 'updatePopup' ? request.reason : request.method) {
|
|
||||||
|
|
||||||
case 'styleDeleted':
|
case 'styleDeleted':
|
||||||
removeStyle(request.id, document);
|
removeStyle(request.id, document);
|
||||||
|
@ -80,8 +79,10 @@ function applyOnMessage(request, sender, sendResponse) {
|
||||||
replaceAll(request.styles, document);
|
replaceAll(request.styles, document);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'styleDisableAll':
|
case 'prefChanged':
|
||||||
doDisableAll(request.disableAll);
|
if ('disableAll' in request.prefs) {
|
||||||
|
doDisableAll(request.prefs.disableAll);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ping':
|
case 'ping':
|
||||||
|
|
|
@ -72,14 +72,13 @@ function onBackgroundMessage(request, sender, sendResponse) {
|
||||||
() => sendResponse(false));
|
() => sendResponse(false));
|
||||||
return KEEP_CHANNEL_OPEN;
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
case 'styleDisableAll':
|
|
||||||
request = {prefName: 'disableAll', value: request.disableAll};
|
|
||||||
// fallthrough to prefChanged
|
|
||||||
|
|
||||||
case 'prefChanged':
|
case 'prefChanged':
|
||||||
// eslint-disable-next-line no-use-before-define
|
for (var prefName in request.prefs) { // eslint-disable-line no-var
|
||||||
if (typeof request.value == 'boolean' && contextMenus[request.prefName]) {
|
if (prefName in contextMenus) { // eslint-disable-line no-use-before-define
|
||||||
chrome.contextMenus.update(request.prefName, {checked: request.value}, ignoreChromeError);
|
chrome.contextMenus.update(prefName, {
|
||||||
|
checked: request.prefs[prefName],
|
||||||
|
}, ignoreChromeError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
4
edit.js
4
edit.js
|
@ -1824,8 +1824,8 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "prefChanged":
|
case "prefChanged":
|
||||||
if (request.prefName == "editor.smartIndent") {
|
if ('editor.smartIndent' in request.prefs) {
|
||||||
CodeMirror.setOption("smartIndent", request.value);
|
CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'editDeleteText':
|
case 'editDeleteText':
|
||||||
|
|
13
messaging.js
13
messaging.js
|
@ -20,17 +20,14 @@ function notifyAllTabs(request) {
|
||||||
updateIcon(tab);
|
updateIcon(tab);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// notify all open popups
|
|
||||||
const reqPopup = Object.assign({}, request, {method: 'updatePopup', reason: request.method});
|
|
||||||
chrome.runtime.sendMessage(reqPopup);
|
|
||||||
// notify self: the message no longer is sent to the origin in new Chrome
|
// notify self: the message no longer is sent to the origin in new Chrome
|
||||||
if (typeof applyOnMessage !== 'undefined') {
|
if (window.applyOnMessage) {
|
||||||
applyOnMessage(reqPopup);
|
applyOnMessage(request);
|
||||||
}
|
} else if (window.onBackgroundMessage) {
|
||||||
// notify self: pref changed by background page
|
|
||||||
if (request.method == 'prefChanged' && typeof onBackgroundMessage !== 'undefined') {
|
|
||||||
onBackgroundMessage(request);
|
onBackgroundMessage(request);
|
||||||
}
|
}
|
||||||
|
// notify background page and all open popups
|
||||||
|
chrome.runtime.sendMessage(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
27
popup.js
27
popup.js
|
@ -18,16 +18,22 @@ getActiveTabRealURL().then(url => {
|
||||||
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(msg => {
|
chrome.runtime.onMessage.addListener(msg => {
|
||||||
if (msg.method == 'updatePopup') {
|
switch (msg.method) {
|
||||||
switch (msg.reason) {
|
case 'styleAdded':
|
||||||
case 'styleAdded':
|
case 'styleUpdated':
|
||||||
case 'styleUpdated':
|
handleUpdate(msg.style);
|
||||||
handleUpdate(msg.style);
|
break;
|
||||||
break;
|
case 'styleDeleted':
|
||||||
case 'styleDeleted':
|
handleDelete(msg.id);
|
||||||
handleDelete(msg.id);
|
break;
|
||||||
break;
|
case 'prefChanged':
|
||||||
}
|
if ('popup.stylesFirst' in msg.prefs) {
|
||||||
|
const stylesFirst = msg.prefs['popup.stylesFirst'];
|
||||||
|
const actions = $('body > .actions');
|
||||||
|
const before = stylesFirst ? actions : actions.nextSibling;
|
||||||
|
document.body.insertBefore(installed, before);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,7 +59,6 @@ function initPopup(url) {
|
||||||
$('#popup-options-button').onclick = () => chrome.runtime.openOptionsPage();
|
$('#popup-options-button').onclick = () => chrome.runtime.openOptionsPage();
|
||||||
$('#popup-shortcuts-button').onclick = configureCommands.open;
|
$('#popup-shortcuts-button').onclick = configureCommands.open;
|
||||||
|
|
||||||
// styles first?
|
|
||||||
if (!prefs.get('popup.stylesFirst')) {
|
if (!prefs.get('popup.stylesFirst')) {
|
||||||
document.body.insertBefore(
|
document.body.insertBefore(
|
||||||
$('body > .actions'),
|
$('body > .actions'),
|
||||||
|
|
157
storage.js
157
storage.js
|
@ -497,7 +497,7 @@ function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirs
|
||||||
// and as Map in case the string is not the same reference used to add the item
|
// and as Map in case the string is not the same reference used to add the item
|
||||||
//const t0start = performance.now();
|
//const t0start = performance.now();
|
||||||
const code = section.code;
|
const code = section.code;
|
||||||
let isEmpty = code.length < 1000 && cachedStyles.emptyCode.get(code);
|
let isEmpty = code !== null && code.length < 1000 && cachedStyles.emptyCode.get(code);
|
||||||
if (isEmpty === undefined) {
|
if (isEmpty === undefined) {
|
||||||
isEmpty = !code || !code.trim()
|
isEmpty = !code || !code.trim()
|
||||||
|| code.indexOf('@namespace') >= 0
|
|| code.indexOf('@namespace') >= 0
|
||||||
|
@ -540,9 +540,18 @@ function tryRegExp(regexp) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
prefs = prefs || new function Prefs() {
|
function debounce(fn, ...args) {
|
||||||
const me = this;
|
const timers = debounce.timers = debounce.timers || new Map();
|
||||||
|
debounce.run = debounce.run || ((fn, ...args) => {
|
||||||
|
timers.delete(fn);
|
||||||
|
fn(...args);
|
||||||
|
});
|
||||||
|
clearTimeout(timers.get(fn));
|
||||||
|
timers.set(fn, setTimeout(debounce.run, 0, fn, ...args));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
prefs = prefs || new function Prefs() {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
'openEditInWindow': false, // new editor opens in a own browser window
|
'openEditInWindow': false, // new editor opens in a own browser window
|
||||||
'windowPosition': {}, // detached window position
|
'windowPosition': {}, // detached window position
|
||||||
|
@ -589,85 +598,85 @@ prefs = prefs || new function Prefs() {
|
||||||
};
|
};
|
||||||
const values = deepCopy(defaults);
|
const values = deepCopy(defaults);
|
||||||
|
|
||||||
let syncTimeout; // see broadcast() function below
|
// coalesce multiple pref changes in broadcast
|
||||||
|
let broadcastPrefs = {};
|
||||||
|
|
||||||
|
function doBroadcast() {
|
||||||
|
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs});
|
||||||
|
broadcastPrefs = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSyncSet() {
|
||||||
|
getSync().set({'settings': values});
|
||||||
|
}
|
||||||
|
|
||||||
Object.defineProperty(this, 'readOnlyValues', {value: {}});
|
Object.defineProperty(this, 'readOnlyValues', {value: {}});
|
||||||
|
|
||||||
Prefs.prototype.get = function(key, defaultValue) {
|
Object.assign(Prefs.prototype, {
|
||||||
if (key in values) {
|
|
||||||
return values[key];
|
|
||||||
}
|
|
||||||
if (defaultValue !== undefined) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
if (key in defaults) {
|
|
||||||
return defaults[key];
|
|
||||||
}
|
|
||||||
console.warn("No default preference for '%s'", key);
|
|
||||||
};
|
|
||||||
|
|
||||||
Prefs.prototype.getAll = function() {
|
get(key, defaultValue) {
|
||||||
return deepCopy(values);
|
if (key in values) {
|
||||||
};
|
return values[key];
|
||||||
|
}
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
if (key in defaults) {
|
||||||
|
return defaults[key];
|
||||||
|
}
|
||||||
|
console.warn("No default preference for '%s'", key);
|
||||||
|
},
|
||||||
|
|
||||||
Prefs.prototype.set = function(key, value, options) {
|
getAll() {
|
||||||
const oldValue = deepCopy(values[key]);
|
return deepCopy(values);
|
||||||
values[key] = value;
|
},
|
||||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
|
||||||
if ((!options || !options.noBroadcast) && !equal(value, oldValue)) {
|
|
||||||
me.broadcast(key, value, options);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Prefs.prototype.remove = key => me.set(key, undefined);
|
set(key, value, {noBroadcast, noSync} = {}) {
|
||||||
|
const oldValue = deepCopy(values[key]);
|
||||||
|
values[key] = value;
|
||||||
|
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||||
|
if (!noBroadcast && !equal(value, oldValue)) {
|
||||||
|
this.broadcast(key, value, {noSync});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
Prefs.prototype.broadcast = function(key, value, options) {
|
remove: key => this.set(key, undefined),
|
||||||
const message = {method: 'prefChanged', prefName: key, value: value};
|
|
||||||
notifyAllTabs(message);
|
|
||||||
chrome.runtime.sendMessage(message);
|
|
||||||
if (key == 'disableAll') {
|
|
||||||
notifyAllTabs({method: 'styleDisableAll', disableAll: value});
|
|
||||||
}
|
|
||||||
if (!options || !options.noSync) {
|
|
||||||
clearTimeout(syncTimeout);
|
|
||||||
syncTimeout = setTimeout(function() {
|
|
||||||
getSync().set({'settings': values});
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(defaults).forEach(function(key) {
|
broadcast(key, value, {noSync} = {}) {
|
||||||
me.set(key, defaults[key], {noBroadcast: true});
|
broadcastPrefs[key] = value;
|
||||||
|
debounce(doBroadcast);
|
||||||
|
if (!noSync) {
|
||||||
|
debounce(doSyncSet);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
getSync().get('settings', function(result) {
|
Object.keys(defaults).forEach(key => {
|
||||||
const synced = result.settings;
|
this.set(key, defaults[key], {noBroadcast: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
getSync().get('settings', ({settings: synced}) => {
|
||||||
for (const key in defaults) {
|
for (const key in defaults) {
|
||||||
if (synced && (key in synced)) {
|
if (synced && (key in synced)) {
|
||||||
me.set(key, synced[key], {noSync: true});
|
this.set(key, synced[key], {noSync: true});
|
||||||
} else {
|
|
||||||
const value = tryMigrating(key);
|
|
||||||
if (value !== undefined) {
|
|
||||||
me.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof contextMenus !== 'undefined') {
|
if (typeof contextMenus !== 'undefined') {
|
||||||
for (const id in contextMenus) {
|
for (const id in contextMenus) {
|
||||||
if (typeof values[id] == 'boolean') {
|
if (typeof values[id] == 'boolean') {
|
||||||
me.broadcast(id, values[id], {noSync: true});
|
this.broadcast(id, values[id], {noSync: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.storage.onChanged.addListener(function(changes, area) {
|
chrome.storage.onChanged.addListener((changes, area) => {
|
||||||
if (area == 'sync' && 'settings' in changes) {
|
if (area == 'sync' && 'settings' in changes) {
|
||||||
const synced = changes.settings.newValue;
|
const synced = changes.settings.newValue;
|
||||||
if (synced) {
|
if (synced) {
|
||||||
for (const key in defaults) {
|
for (const key in defaults) {
|
||||||
if (key in synced) {
|
if (key in synced) {
|
||||||
me.set(key, synced[key], {noSync: true});
|
this.set(key, synced[key], {noSync: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -676,29 +685,6 @@ prefs = prefs || new function Prefs() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function tryMigrating(key) {
|
|
||||||
if (!(key in localStorage)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const value = localStorage[key];
|
|
||||||
delete localStorage[key];
|
|
||||||
localStorage['DEPRECATED: ' + key] = value;
|
|
||||||
switch (typeof defaults[key]) {
|
|
||||||
case 'boolean':
|
|
||||||
return value.toLowerCase() === 'true';
|
|
||||||
case 'number':
|
|
||||||
return Number(value);
|
|
||||||
case 'object':
|
|
||||||
try {
|
|
||||||
return JSON.parse(value);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Cannot migrate from localStorage %s = '%s': %o", key, value, e);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}();
|
}();
|
||||||
|
|
||||||
|
|
||||||
|
@ -712,14 +698,18 @@ function setupLivePrefs(IDs) {
|
||||||
prefs.set(this.id, isCheckbox(this) ? this.checked : this.value);
|
prefs.set(this.id, isCheckbox(this) ? this.checked : this.value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
chrome.runtime.onMessage.addListener(function(request) {
|
chrome.runtime.onMessage.addListener(msg => {
|
||||||
if (request.prefName in localIDs) {
|
if (msg.prefs) {
|
||||||
updateElement(request.prefName);
|
for (const prefName in msg.prefs) {
|
||||||
|
if (prefName in localIDs) {
|
||||||
|
updateElement(prefName, msg.prefs[prefName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
function updateElement(id) {
|
function updateElement(id, value) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
el[isCheckbox(el) ? 'checked' : 'value'] = prefs.get(id);
|
el[isCheckbox(el) ? 'checked' : 'value'] = value || prefs.get(id);
|
||||||
el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
@ -818,6 +808,7 @@ function defineReadonlyProperty(obj, key, value) {
|
||||||
Object.defineProperty(obj, key, {value: copy, configurable: true});
|
Object.defineProperty(obj, key, {value: copy, configurable: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
|
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
|
||||||
function getSync() {
|
function getSync() {
|
||||||
if ('sync' in chrome.storage) {
|
if ('sync' in chrome.storage) {
|
||||||
|
|
|
@ -106,7 +106,7 @@ window.setTimeout(function () {
|
||||||
}
|
}
|
||||||
chrome.runtime.onMessage.addListener(request => {
|
chrome.runtime.onMessage.addListener(request => {
|
||||||
// when user has changed the predefined time interval in the settings page
|
// when user has changed the predefined time interval in the settings page
|
||||||
if (request.method === 'prefChanged' && request.prefName === 'updateInterval') {
|
if (request.method === 'prefChanged' && 'updateInterval' in request.prefs) {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
// when user just manually checked for updates
|
// when user just manually checked for updates
|
||||||
|
|
Loading…
Reference in New Issue
Block a user