Skip to content

Commit 8bea01b

Browse files
committed
Speed & accuracy improvements to pseudo element rendering.
Previously, pseudo elements would be processed as they were found in the DOM tree, which was an expensive operation as each element's computed :before and :after style was checked for 'content' styles. This commit traverses the user's stylesheets for :before and :after selectors, gathers the classes affected, selects all elements that likely have a pseudo element present, then checks computed style. If there is actually an element present, it is created but *not* appended to the DOM until after all elements have been processed. After all elements have been found and created, they are added to the DOM in a single batch, and the original pseudo elements are hidden in a single batch. This prevents the layout invalidation / relayout loop that was occuring previously, and in my tests speeds parsing by as much as 50% or more, depending on how many pseudo elements your page uses. Additionally, this commit contains a bugfix to the handling of ":before" pseudo elements; the browser effectively inserts them as the first child of the element, not before the element. This fixes a few rendering inconsistencies and complicated pages look almost perfect in my tests.
1 parent e115180 commit 8bea01b

File tree

3 files changed

+305
-101
lines changed

3 files changed

+305
-101
lines changed

build/html2canvas.js

Lines changed: 152 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,36 +1041,162 @@ _html2canvas.Parse = function (images, options, cb) {
10411041
body = doc.body,
10421042
getCSS = Util.getCSS,
10431043
pseudoHide = "___html2canvas___pseudoelement",
1044-
hidePseudoElements = doc.createElement('style');
1044+
hidePseudoElementsStyles = doc.createElement('style');
10451045

1046-
hidePseudoElements.innerHTML = '.' + pseudoHide + '-before:before { content: "" !important; display: none !important; }' +
1047-
'.' + pseudoHide + '-after:after { content: "" !important; display: none !important; }';
1046+
hidePseudoElementsStyles.innerHTML = '.' + pseudoHide +
1047+
'-parent:before { content: "" !important; display: none !important; }' +
1048+
'.' + pseudoHide + '-parent:after { content: "" !important; display: none !important; }';
10481049

1049-
body.appendChild(hidePseudoElements);
1050+
body.appendChild(hidePseudoElementsStyles);
10501051

10511052
images = images || {};
10521053

1054+
init();
1055+
10531056
function init() {
10541057
var background = getCSS(document.documentElement, "backgroundColor"),
10551058
transparentBackground = (Util.isTransparent(background) && element === document.body),
10561059
stack = renderElement(element, null, false, transparentBackground);
1060+
1061+
// create pseudo elements in a single pass to prevent synchronous layouts
1062+
addPseudoElements(element);
10571063

1058-
parseChildren(element, stack, null, function() {
1064+
parseChildren(element, stack, function() {
10591065
if (transparentBackground) {
10601066
background = stack.backgroundColor;
10611067
}
10621068

1069+
removePseudoElements();
1070+
10631071
Util.log('Done parsing, moving to Render.');
10641072

1065-
body.removeChild(hidePseudoElements);
10661073
cb({
10671074
backgroundColor: background,
10681075
stack: stack
10691076
});
10701077
});
10711078
}
10721079

1073-
init();
1080+
// Given a root element, find all pseudo elements below, create elements mocking pseudo element styles
1081+
// so we can process them as normal elements, and hide the original pseudo elements so they don't interfere
1082+
// with layout.
1083+
function addPseudoElements(el) {
1084+
// These are done in discrete steps to prevent a relayout loop caused by addClass() invalidating
1085+
// layouts & getPseudoElement calling getComputedStyle.
1086+
var jobs = [], classes = [];
1087+
getPseudoElementClasses();
1088+
findPseudoElements(el);
1089+
runJobs();
1090+
1091+
function getPseudoElementClasses(){
1092+
var findPsuedoEls = /:before|:after/;
1093+
var sheets = document.styleSheets;
1094+
for (var i = 0, j = sheets.length; i < j; i++) {
1095+
try {
1096+
var rules = sheets[i].cssRules;
1097+
for (var k = 0, l = rules.length; k < l; k++) {
1098+
if(findPsuedoEls.test(rules[k].selectorText)) {
1099+
classes.push(rules[k].selectorText);
1100+
}
1101+
}
1102+
}
1103+
catch(e) { // will throw security exception for style sheets loaded from external domains
1104+
}
1105+
}
1106+
1107+
// Trim off the :after and :before (or ::after and ::before)
1108+
for (i = 0, j = classes.length; i < j; i++) {
1109+
classes[i] = classes[i].match(/(^[^:]*)/)[1];
1110+
}
1111+
}
1112+
1113+
// Using the list of elements we know how pseudo el styles, create fake pseudo elements.
1114+
function findPseudoElements(el) {
1115+
var els = document.querySelectorAll(classes.join(','));
1116+
for(var i = 0, j = els.length; i < j; i++) {
1117+
createPseudoElements(els[i]);
1118+
}
1119+
}
1120+
1121+
// Create pseudo elements & add them to a job queue.
1122+
function createPseudoElements(el) {
1123+
var before = getPseudoElement(el, ':before'),
1124+
after = getPseudoElement(el, ':after');
1125+
1126+
if(before) {
1127+
jobs.push({type: 'before', pseudo: before, el: el});
1128+
}
1129+
1130+
if (after) {
1131+
jobs.push({type: 'after', pseudo: after, el: el});
1132+
}
1133+
}
1134+
1135+
// Adds a class to the pseudo's parent to prevent the original before/after from messing
1136+
// with layouts.
1137+
// Execute the inserts & addClass() calls in a batch to prevent relayouts.
1138+
function runJobs() {
1139+
// Add Class
1140+
jobs.forEach(function(job){
1141+
addClass(job.el, pseudoHide + "-parent");
1142+
});
1143+
1144+
// Insert el
1145+
jobs.forEach(function(job){
1146+
if(job.type === 'before'){
1147+
job.el.insertBefore(job.pseudo, job.el.firstChild);
1148+
} else {
1149+
job.el.appendChild(job.pseudo);
1150+
}
1151+
});
1152+
}
1153+
}
1154+
1155+
1156+
1157+
// Delete our fake pseudo elements from the DOM. This will remove those actual elements
1158+
// and the classes on their parents that hide the actual pseudo elements.
1159+
// Note that NodeLists are 'live' collections so you can't use a for loop here. They are
1160+
// actually deleted from the NodeList after each iteration.
1161+
function removePseudoElements(){
1162+
// delete pseudo elements
1163+
body.removeChild(hidePseudoElementsStyles);
1164+
var pseudos = document.getElementsByClassName(pseudoHide + "-element");
1165+
while (pseudos.length) {
1166+
pseudos[0].parentNode.removeChild(pseudos[0]);
1167+
}
1168+
1169+
// Remove pseudo hiding classes
1170+
var parents = document.getElementsByClassName(pseudoHide + "-parent");
1171+
while(parents.length) {
1172+
removeClass(parents[0], pseudoHide + "-parent");
1173+
}
1174+
}
1175+
1176+
function addClass (el, className) {
1177+
if (el.classList) {
1178+
el.classList.add(className);
1179+
} else {
1180+
el.className = el.className + " " + className;
1181+
}
1182+
}
1183+
1184+
function removeClass (el, className) {
1185+
if (el.classList) {
1186+
el.classList.remove(className);
1187+
} else {
1188+
el.className = el.className.replace(className, "").trim();
1189+
}
1190+
}
1191+
1192+
function hasClass (el, className) {
1193+
return el.className.indexOf(className) > -1;
1194+
}
1195+
1196+
// Note that this doesn't work in < IE8, but we don't support that anyhow
1197+
function nodeListToArray (nodeList) {
1198+
return Array.prototype.slice.call(nodeList);
1199+
}
10741200

10751201
function documentWidth () {
10761202
return Math.max(
@@ -1831,20 +1957,25 @@ _html2canvas.Parse = function (images, options, cb) {
18311957

18321958
function getPseudoElement(el, which) {
18331959
var elStyle = window.getComputedStyle(el, which);
1834-
if(!elStyle || !elStyle.content || elStyle.content === "none" || elStyle.content === "-moz-alt-content" || elStyle.display === "none") {
1960+
var parentStyle = window.getComputedStyle(el);
1961+
// If no content attribute is present, the pseudo element is hidden,
1962+
// or the parent has a content property equal to the content on the pseudo element,
1963+
// move along.
1964+
if(!elStyle || !elStyle.content || elStyle.content === "none" || elStyle.content === "-moz-alt-content" ||
1965+
elStyle.display === "none" || parentStyle.content === elStyle.content) {
18351966
return;
18361967
}
1837-
var content = elStyle.content + '',
1838-
first = content.substr( 0, 1 );
1839-
//strips quotes
1840-
if(first === content.substr( content.length - 1 ) && first.match(/'|"/)) {
1841-
content = content.substr( 1, content.length - 2 );
1968+
var content = elStyle.content + '';
1969+
1970+
// Strip inner quotes
1971+
if(content[0] === "'" || content[0] === "\"") {
1972+
content = content.replace(/(^['"])|(['"]$)/g, '');
18421973
}
18431974

18441975
var isImage = content.substr( 0, 3 ) === 'url',
18451976
elps = document.createElement( isImage ? 'img' : 'span' );
18461977

1847-
elps.className = pseudoHide + "-before " + pseudoHide + "-after";
1978+
elps.className = pseudoHide + "-element ";
18481979

18491980
Object.keys(elStyle).filter(indexedProperty).forEach(function(prop) {
18501981
// Prevent assigning of read only CSS Rules, ex. length, parentRule
@@ -1867,31 +1998,6 @@ _html2canvas.Parse = function (images, options, cb) {
18671998
return (isNaN(window.parseInt(property, 10)));
18681999
}
18692000

1870-
function injectPseudoElements(el, stack) {
1871-
var before = getPseudoElement(el, ':before'),
1872-
after = getPseudoElement(el, ':after');
1873-
if(!before && !after) {
1874-
return;
1875-
}
1876-
1877-
if(before) {
1878-
el.className += " " + pseudoHide + "-before";
1879-
el.parentNode.insertBefore(before, el);
1880-
parseElement(before, stack, true);
1881-
el.parentNode.removeChild(before);
1882-
el.className = el.className.replace(pseudoHide + "-before", "").trim();
1883-
}
1884-
1885-
if (after) {
1886-
el.className += " " + pseudoHide + "-after";
1887-
el.appendChild(after);
1888-
parseElement(after, stack, true);
1889-
el.removeChild(after);
1890-
el.className = el.className.replace(pseudoHide + "-after", "").trim();
1891-
}
1892-
1893-
}
1894-
18952001
function renderBackgroundRepeat(ctx, image, backgroundPosition, bounds) {
18962002
var offsetX = Math.round(bounds.left + backgroundPosition.left),
18972003
offsetY = Math.round(bounds.top + backgroundPosition.top);
@@ -2079,7 +2185,7 @@ _html2canvas.Parse = function (images, options, cb) {
20792185
return bounds;
20802186
}
20812187

2082-
function renderElement(element, parentStack, pseudoElement, ignoreBackground) {
2188+
function renderElement(element, parentStack, ignoreBackground) {
20832189
var transform = getTransform(element, parentStack),
20842190
bounds = getBounds(element, transform),
20852191
image,
@@ -2109,10 +2215,6 @@ _html2canvas.Parse = function (images, options, cb) {
21092215
renderBorders(ctx, border.args, border.color);
21102216
});
21112217

2112-
if (!pseudoElement) {
2113-
injectPseudoElements(element, stack);
2114-
}
2115-
21162218
switch(element.nodeName){
21172219
case "IMG":
21182220
if ((image = loadImage(element.getAttribute('src')))) {
@@ -2153,20 +2255,20 @@ _html2canvas.Parse = function (images, options, cb) {
21532255
return (getCSS(element, 'display') !== "none" && getCSS(element, 'visibility') !== "hidden" && !element.hasAttribute("data-html2canvas-ignore"));
21542256
}
21552257

2156-
function parseElement (element, stack, pseudoElement, cb) {
2258+
function parseElement (element, stack, cb) {
21572259
if (!cb) {
21582260
cb = function(){};
21592261
}
21602262
if (isElementVisible(element)) {
2161-
stack = renderElement(element, stack, pseudoElement, false) || stack;
2263+
stack = renderElement(element, stack, false) || stack;
21622264
if (!ignoreElementsRegExp.test(element.nodeName)) {
2163-
return parseChildren(element, stack, pseudoElement, cb);
2265+
return parseChildren(element, stack, cb);
21642266
}
21652267
}
21662268
cb();
21672269
}
21682270

2169-
function parseChildren(element, stack, pseudoElement, cb) {
2271+
function parseChildren(element, stack, cb) {
21702272
var children = Util.Children(element);
21712273
// After all nodes have processed, finished() will call the cb.
21722274
// We add one and kick it off so this will still work when children.length === 0.
@@ -2185,7 +2287,7 @@ _html2canvas.Parse = function (images, options, cb) {
21852287

21862288
function parseNode(node) {
21872289
if (node.nodeType === node.ELEMENT_NODE) {
2188-
parseElement(node, stack, pseudoElement, finished);
2290+
parseElement(node, stack, finished);
21892291
} else if (node.nodeType === node.TEXT_NODE) {
21902292
renderText(element, node, stack);
21912293
finished();
@@ -2706,9 +2808,9 @@ window.html2canvas = function(elements, opts) {
27062808
useOverflow: true,
27072809
letterRendering: false,
27082810
chinese: false,
2811+
async: false, // If true, parsing will not block, but if the user scrolls during parse the image can get weird
27092812

27102813
// render options
2711-
27122814
width: null,
27132815
height: null,
27142816
taintTest: true, // do a taint test with all images before applying to canvas

0 commit comments

Comments
 (0)