I want to make a reactjs page lazy loading images. Therefore I call lazyLoadImages() inside the componentDidMount(). I found that the browser loading indicator located at browser tab still keep spinning all the way until all images are loaded. This make me think what I've done does not provide a true lazy load image experience.
The interesting thing is that if I wrap lazyLoadImages() in setTimeout with a not too short timeout argument, such as 100ms (not 0 or even 50ms), then the page loading indicator will very soon stop spinning and disappear, which gives me a feeling that the page DOM is complete while those images are started to load at a background process.
I thought the componentDidMount and window onload are something similar, but they are not. I can have the same experience as using setTimeout by using the window onload event. But since my application is a SPA, therefore onload event is not suitable. Because onload event only work when I explicitly refresh this page, but not navigating between pages using react-router.
Anyone has idea on this phenomenon and how can I achieve the same without using setTimeout function?
componentDidMount() {
setTimeout(this.lazyLoadCarouselImage, 100);
// or invoke directly -> this.lazyLoadCarouselImage();
}
lazyLoadCarouselImage() {
let images = [];
let loadedCounter = 0;
let lazyLoadImageList = ['https://wallpaperaccess.com/full/637960.jpg','https://wallpaperaccess.com/full/637960.jpg']
for (let i=0; i<lazyLoadImageList.length; i++ ) {
images[i] = new Image();
images[i].onload = ()=> {
loadedCounter++;
if (loadedCounter == lazyLoadImageList.length) {
// update state here saying all images are loaded
}
}
images[i].src = lazyLoadImageList[i];
}
}
There are libraries that can handle the lazy loading using scroll and resize event handlers, while others use Intersection Observer API. Check out Lazy-loading images for details.
Nowadays you can lazy load images simply by adding the attribute loading="lazy" to each <img> element. Just keep in mind that the feature is fairly new, so make sure potential users are using an up to date Browser.
Below is a quick example where I create 100 images that are "lazy loaded":
class App extends React.Component {
render() {
let results = [];
for (let i = 0; i < 100; i++) {
results.push(
<img
key={`image${i}`}
src={`https://placehold.it/4${i < 10 ? `0${i}` : i }x4${i < 10 ? `0${i}` : i }`}
alt="placeholder"
width={`4${i < 10 ? `0${i}` : i }`}
height={`4${i < 10 ? `0${i}` : i }`}
loading="lazy"
/>
)
}
return (
<div style={{ width: '400px' }}>
{results}
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Note: Click "Run code snippet", then open the "Network" tab inside your Browser's developer tools, begin to scroll and you should notice how the images load as they become close to the viewport.
seems it's working to me
const [isLoaded, setIsLoaded] = useState(false);
const [isPageLoaded, setIsPageLoaded] = useState(false); //this helps with lazy loading
useEffect(() => {
setIsLoaded(true);
}, []);
useEffect(() => {
if (isLoaded) {
setIsPageLoaded(true);
}
}, [isLoaded]);
Related
I have created a react portal inside my application to handle the use of Modal. The portal target is outside of my React root div as sibling of my root element.
<html>
<body>
<div id="root">{{app.html}}</div>
<div id="modal-root">
<div class="modal" tabIndex="-1" id="modal-inner-root" role="dialog">
</div>
</div>
</body>
</html>
So my Portal contents renders outside of the react application and its working fine. Here is my react portal code
const PortalRawModal = (props) => {
const [ display, setDisplay ] = useState(document.getElementById("modal-inner-root").style.display)
const div = useRef(document.createElement('div'))
useEffect(()=> {
const modalInnerRoot = document.getElementById("modal-inner-root")
if(validate(props.showModalId)) {
if( props.showModalId == props.modalId && _.size(props.children) > 0 ) {
setDisplay("block");
if(_.size(modalInnerRoot.childNodes) > 0) {
modalInnerRoot.replaceChild(div.current,modalInnerRoot.childNodes[0]);
} else {
modalInnerRoot.appendChild(div.current);
}
div.current.className = props.modalInner;
document.getElementById("modal-root").className = props.modalClassName;
document.body.className = "modal-open";
} else {
document.getElementById("modal-root").className = props.modalClassName;
if(div.current.parentNode == modalInnerRoot) {
modalInnerRoot.removeChild(div.current);
div.current.className = "";
}
}
} else {
setDisplay("none");
document.getElementById("modal-root").className = "";
if(div.current.parentNode == modalInnerRoot) {
modalInnerRoot.removeChild(div.current).className = "";
}
document.body.className = "";
}
},[ props.showModalId ])
useEffect(()=> {
document.body.className = display == "none" ? "" : "modal-open";
document.getElementById("modal-inner-root").style.display = display;
return () => {
if(!validate(props.showModalId)) {
document.body.className = "";
document.getElementById("modal-inner-root").style.display = "none"
}
};
},[ display])
useEffect(()=> {
if(_.size(props.children) <= 0){
modalInnerRoot.removeChild(div.current)
document.body.className = "";
document.getElementById("modal-inner-root").style.display = "none";
}
return () => {
if(_.size(props.children) <= 0){
modalInnerRoot.removeChild(div.current)
document.body.className = "";
document.getElementById("modal-inner-root").style.display = "none";
}
}
},[props.children, props.showModalId])
return ReactDOM.createPortal(props.children ,div.current);
}
Whenever the children are passed and modal is mounted, The heavy DOM is painted with little delay. But the same markup takes time, or even crashes the browser tab. Where am I going wrong in handling the heavy DOM operations? Or is there any async way to handle the heavy DOM operations that wont effect the overall performance?
Couple of reasons can attribute for this :
The last effect will always run for every re-render as props.children is an object and hence even if same children was passed again, it'll be a new object.
Direct DOM manipulation is an anti-pattern, as React maintains several DOM references in memory for fast diffing, hence direct mutation may result in some perf hit.Try writing the same in a declarative fashion.
Extract out the portal content into another sub-component and avoid DOM manipulations wherever possible.
One place would be :
if (_.size(props.children) <= 0) {
modalInnerRoot.removeChild(div.current);
}
can be replaced within the render function like :
{React.Children.count(props.children) ? <div /> : null}
You just have to use the modal root as the createPortal host div (the second argument). React will just render there instead of in the regular element.
Then if you need to "manipulate" the HTML, just use plain React. It does not work any differently inside of portaled elements. All createPortal does is tell React to take this part of the tree and attach it under specified element.
Just include a permanent empty div with no style (no need to use any display rules), and it will just receive all HTML you want to render. Then you make the content inside the modal root fixed, but not the modal root itself.
Don't do this:
const div = useRef(document.createElement('div'))
// all kinds of manipulation of this div
return ReactDOM.createPortal(props.children ,div.current);
Do this instead:
const modalRoot = document.getElementById('modal-root');
function ModalWrapper({children}) {
return <div class="modal" id="modal-inner-root" role="dialog">
{ children }
</div>
}
function PortalModal({children}) {
return React.createPortal(
<ModalWrapper>{ children }</ModalWrapper>,
modalRoot
}
function App() {
const [hasConfirmed, setHasConfirmed] = useState(false);
return <div>
// ...
{ !hasConfirmed && <PortalModal>
<h1> Please confirm </h1>
<button onClick={() => setHasConfirmed(true)}>
Yes
</button>
</PortalModal> }
</div>
}
You can perfectly manage the state of the component in the modal, and whether it should show the modal at all. You won't need to do any manual DOM manipulation anymore.
Once you stop rendering the portal, React removes it for you.
Why does the question's code have performance issues?
It does many DOM operations which, among other things, will result in style recalculations. You shouldn't have to do manual DOM operations at all, it's exactly what React is built to handle for you. And it's reasonably efficient at it.
Since you're doing it in useEffect, React has already triggered style recalculations and the result of that was painted to the screen. This is now immediately invalidated, and the browser needs to recalculate some amount of elements.
Which amount of elements?... All of them, because a style recalculation on the body is triggered by changing its classname.
document.body.className = "modal-open";
If you have a heavy DOM, a full style recalculation can quickly take long and cause noticeable stutter. You can avoid this by not touching the body and just adding an overlay div you can show and hide.
Can it cause a tab to crash though? Maybe in extreme cases, but probably not.
It's more likely that you're calling this component in a way that creates an infinite loop. Or you may be ending up doing a ridiculous amount of DOM operations. It's impossible to tell without the full code used when the performance issues were noted.
I am using Facebook's like button as generated by facebook's like button configurator. However in order to get facebook-sdk to finish loading before the Like button, I had to use something called react-load-script and make a my own wrapper component for the like button html I got from the configurator.
my like button
class Like extends React.Component {
state = {
facebookLoaded: false
};
handleFacebookLoaded = () => this.setState({
facebookLoaded: true
});
FacebookSDK = () => <>
<div id="fb-root"></div>
<Script
async defer crossOrigin="anonymous"
url="https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v3.3&appId=391623981325884&autoLogAppEvents=1"
onLoad={this.handleFacebookLoaded}
/>
</>;
render() {
return <>
<this.FacebookSDK />
{this.state.facebookLoaded
? <div class="fb-like" data-href={this.props.url} data-width="" data-layout="button_count" data-action="like" data-size="large" data-show-faces="true" data-share="true" />
: null}
</>;
}
}
In my code all the script loading stuff actually happens in App.jsx, but I moved it into one class just to show a simple version.
This part seems to work fine, the issue lies when changing the url passed to data-href.
I checked the react dom in the browser and the data-href is actually being updated properly, however this does not affect the actual url that is being used by the like button, unless I do a full page refresh. I'm assuming this has to do with how the data-href is being used by facebook-sdk. (edit: after testing I'm not sure anymore)
I've found many questions about this on Stack Overflow, however none of them seem to be based off the CDN version of facebook buttons
From what I understand, the div containing the href needs to be placed out and back into the DOM in order for the facebook-sdk to detect a change, but I don't know how to do this in react without a full page refresh. Also I'm not certain this is even the right solution.
-- Update --
I just noticed something else that seems like useful information. If I navigate to the page with the like button, then it doesn't show up. It will only show up if the page refreshes. I tested it by moving the part that loads the script into the like component (like in the example shown above) and that didn't change the behavior at all.
-- more experimenting --
I wrote an event handler that takes all the facebook related jsx out of the dom and back in (by toggling a button) However when all the code goes back into the dom (both jsx and html), the UI for the button does not come back. I'm really now sure how this is possible as I'm literally reloading the script and everything facebook related so this should be equivalent to a page refresh no?
I fixed the issue thanks to misorude. The part I was missing was calling window.FB.XFBML.parse(). I didn't realize I could access FB the same way using the CDN. If anyone is looking for a react solution here is the working code:
class Like extends React.Component {
constructor(props) {
super(props);
this.state = {
url: props.url,
}
}
handleChangePage() {
let likeBtn = document.createElement('div');
likeBtn.className = "fb-like";
likeBtn.setAttribute("data-href", this.props.url);
likeBtn.setAttribute("data-width", "");
likeBtn.setAttribute("data-layout", "button_count");
likeBtn.setAttribute("data-action", "like");
likeBtn.setAttribute("data-size", "large");
likeBtn.setAttribute("data-show-faces", "true");
likeBtn.setAttribute("data-share", "true");
let likePanel = document.getElementById("like-panel");
likePanel.removeChild(likePanel.childNodes[0]);
likePanel.appendChild(likeBtn);
window.FB.XFBML.parse(likePanel)
this.setState({ url: this.props.url });
}
componentDidMount() {
this.handleChangePage();
}
render() {
if(this.props.url !== this.state.url)
this.handleChangePage();
return <div id="like-panel">
{this.props.facebookLoaded
? <div className="fb-like" data-href={this.props.url} data-width="" data-layout="button_count" data-action="like" data-size="large" data-show-faces="true" data-share="true" />
: null}
</div>;
}
}
I moved the CDN out of this component so that it only loads the sdk once for the whole app.
I have a React control that renders a bunch of images. My goal is to avoid the flickering that is caused by an unknown time it takes React to load the images (yes, I know about inline image loading, let's pretend it doesn't exist for a moment)
I have an initialized array in my class:
this.loadedImages = [];
For this purpose I use onLoad in this manner:
render () {
let items = this.props.images.map((value, index) => {
let style = {};
if (this.isImageLoaded(index))
style = value.style;
else
style = {visibility: 'hidden'};
return <img
key={ index }
onClick={ this.onClick }
onLoad={ this.onLoad(index) }
style={ style }
src={ value.image }
alt={ value.alt}/>
});
return (
<div>
{items}
</div>
);
}
}
my onLoad and isImageLoaded look like this:
onLoad = (index) => {
if (!this.isImageLoaded(index)) {
this.loadedImages.push(index);
}
};
isImageLoaded = (index) => {
let isloaded = this.loadedImages.includes(index);
if (isloaded)
console.log(index + " is loaded!");
else
console.log(index + " is NOT loaded ");
return isloaded;
};
The issue is that once my page loads, the images switch from a "not loaded" into a "loaded" mode -- BUT there is only ONE RENDER that occurs before the images are loaded, thus the {visibility: 'hidden'} style remains permanent.
So my page loads without images. Now, if I click my component even once, the images will appear correctly because the component is forced to re-render (since now the images are loaded). BUT there is no option for me to force such a re-draw programmatically from the onLoad function as I'm getting a warning I should not be doing that from render...
My question is: how can I break the chicken/egg problems here and re-render my component once any image completes its loading.
I suggest combining your loadedImages data with the your other image state (as a boolean flag on each) and updating it using setState every time one loads (your headaches are due to this separation and the fact that you are having to manually keep them synchronised).
Then map over the single array of images (including loading state), using something like the src for the key.
I use Webpack 4, Babel 7, React 16.8. My app loads google web fonts, external images required by many components taking part in the initial rendering when users load my pages.
I load fonts with a sass file like this:
#import url('https://fonts.googleapis.com/css?family=Roboto:300,400,700');
I use images within all components like this:
import SearchSvg from '../../images/search_icon.svg';
and use them like this:
<img src={ SearchSvg } />
Now I know about <img onLoad=.....> and I know there are packages out there to test whether web fonts are already loaded. My question is: Is there any SYSTEMIC way/pattern to get the initial rendering of the React components wait until all those external resources are loaded?
Right now I use setTimeout with 500 ms to delay the root rendering in my index.js.
setTimeout(function() {
render(
...
);
}, 500);
I would LOVE to replace this hard-coded value with something that actually knows when everything's loaded -- Ideally without having to Add code in every single Component I use.
The motivation is of course to avoid Font/Image flickering when I initially render my app -- due to the rendering while images/fonts aren't fully loaded yet.
You may render your root component after onload event is fired.
The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets images.
https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
window.onload = function() {
ReactDOM.render(<App/>, document.getElementById('root'));
};
If your purpose is to increase performance then I would highly recommend to consider Server Side Rendering.
In your case, you could use document.fonts.ready to check if the font is ready, and then conditionally render the parts you want once this is true
An example of this is found here:
https://developer.mozilla.org/en-US/docs/Web/API/Document/fonts
For your use case, you could use a similar function found at the above link, then set a state value to true if its ready. Then you could conditionally render what you want once this is true
For example:
Call the function in componentDidMount:
componentDidMount() {
this.isFontLoaded()
}
The function uses document.fonts.ready, which returns a promise. We then set the state value fontReady to true once the promise returns:
isFontLoaded = () => {
document.fonts.ready.then(this.setState({ fontReady: true }))
}
Only render the the things you want if fontReady is true:
{fontReady && <img src={ SearchSvg } />}
You can use a wrapper component to do all the checks and show a loading or nothing when that's happening and when everything is done, render the application. Something like:
class LoadApplication {
state = { loaded: false }
timer = null
componentDidMount() {
this.timer = setInterval(this.checkStuff, 50)
}
componentWillUnmount() {
clearInterval(this.timer)
}
checkStuff() {
if (stuffIsLoaded) {
this.setState({ loaded: true })
}
}
render() {
return this.state.loaded ? this.props.children : null;
}
}
...
ReactDOM.render(<LoadApplication><App /></LoadApplication>, document.querySelector('#root'))
This is kinda the same way CRA handles errors. The same way is recommended to catch errors in components in React's documentation so I think it might be what you're looking for. I hope it helps.
An example of what I am talking about is here: https://detrum-replication.herokuapp.com/
My problem can be seen when the hovering diamond in the bottom left is clicked. I have both the gradient and background rendered conditionally according to a value in the state. It just cycles from 1-4 and changes the class of the divs according to the number of the state. The only thing that I don't like is the split second flash when the diamond is clicked. Once the images have been loaded and cycled through it no longer flashes so I assume the images have been cached. Is there a way to precache the images I will be using for the background using React? Did I go about this in the wrong way to achieve what I am trying to do? I have tried using a few precachers for React with no luck. Some of my code is as follows:
constructor(props) {
super(props);
this.state = {
bg: 1
}
}
changeSeason = () => {
let current = this.state.bg;
if (current >= 4){
current = 1;
} else {
current++;
}
this.setState(() => ({bg : current}));
localStorage.setItem("bg", current);
};
render() {
return (
<div className={`gradient${this.state.bg}`}>
<div className={`background${this.state.bg}`}></div>
<div className="diamond" onClick={this.changeSeason}><div className="diamond__shadow-bottom"></div><div className="diamond__shadow-right"></div></div>
</div>
)
}
Thanks for taking the time to help me.
You can preload assets on your app when you know for sure you would be needing them. The browser doesn't wait for your javascript/css to parse and know that you need them, thereby preventing the flicker.
You need to add them to your index.html like this
<link rel="preload" href="bg-image.png" as="image">
Look at the MDN docs for reference