Articles Projects Tips Downloads Contacts About

Slow JEditorPane/JTextPane and performance improvements.
By Stanislav Lapitsky

Looking in the net from time to time on forums I see posts like “Slow opening pages in JTextPane” or “How to improve performance of big texts showing in JEditorPane”. For me the slow JEditorPane is rather common problem so I investigated this a bit and would like to write some notes to improve the speed. Hope they are useful.

To understand what happens inside I created a custom EditorKit (HTMLEditorit extension) and replaced root view (HTMLBlockView extends BlockView). In the view I overrode layout() method to measure time spent on layout.

    protected void layout(int width, int height) {
        long start=System.currentTimeMillis();
        super.layout(width, height);
        long end=System.currentTimeMillis();
        System.out.println("w="+width+" h="+height+" time="+(end-start));
    }

Then I opened multiple pages and used different calls and methods measuring time spent here and there. The results are:

  1. Scroll pane and content measuring.

    In most cases when we expect big JEditorPane’s content we place it in JScrollPane. That’s potential source of improvement. When something is opened and content changed somehow layout is called. When JEditorPane is added into JScrollPane with default vertical scrollbar policy content is lay outed measured and JScrollPane found that vertical scrollbar is necessary after that content is relayed out once again with smaller width (new width = old width – scrollbar width). So in fact we do the layout twice. One simplest fix is code like this:

                final int old=scroll2.getVerticalScrollBarPolicy();
                scroll2.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        editor.setText(myContent);
                        scroll2.setVerticalScrollBarPolicy(old);
                    }
                });
    

    Thus we set the scroll pane’s size to necessary size and skip one unnecessary layout.

  2. Using setText() vs setPage() methods.

    Every time Document’s structure is changed somehow when something added, removed or attributes changed views are updated so layout() is called. Thus views are relay outed as many times as we change Document’s structure.

    setText() uses old Document. It removes all the old content and adds all the new Elements to created appropriate structure. So the layout() may be called multiple times for each structure change during kit.read().

        public void setText(String t) {
            try {
                Document doc = getDocument();
                doc.remove(0, doc.getLength());
                if (t == null || t.equals("")) {
                    return;
                }
                Reader r = new StringReader(t);
                EditorKit kit = getEditorKit();
                kit.read(r, doc, 0);
            } catch (IOException ioe) {
                UIManager.getLookAndFeel().provideErrorFeedback(JEditorPane.this);
            } catch (BadLocationException ble) {
                UIManager.getLookAndFeel().provideErrorFeedback(JEditorPane.this);
            }
        }

    As against the setPage() method creates a new Document instance and build all the Document’s structures and as the last step calls setDocument(). Using the approach views aren’t refreshed every time Document's element is changed but created and lay outed just once when the Document is set.

        public void setPage(URL page) throws IOException {
        ...
    	    if ((loaded == null) || (! loaded.sameFile(page))) {
    	        // different url, load the new content
    	        InputStream in = getStream(page);
    	        if (kit != null) {
    		    Document doc = kit.createDefaultDocument();
        ...
    		    read(in, doc);
    		    setDocument(doc);  
        ...
    	        }
    	    }
    
        ...
        }

    Actually the doc opening depends on asynchronously/ synchronously loading. But the approach can be used in a custom EditorKit to speed up opening.

  3. setSize() with Integer.MAX_VALUE

    When setText() is called before container frame (or window or dialog) showing and initial size of JEditorPane is unknown UI calls a dummy layout. When JEditorPane is asked for preferred size BasicTextUI checks that size of JEditorPane is 0 and the following method is called.

        public Dimension getPreferredSize(JComponent c) {
            ...
            Dimension d = c.getSize();
     
            ...
                else if (d.width == 0 && d.height == 0) {
                    // Probably haven't been layed out yet, force some sort of
                    // initial sizing.
                    rootView.setSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
                }
            ...
            return d;
        }
    

    So unnecessary layout is done which can waste the opening time because the layout will be removed just after setVisible(true) will be called to provide actual layout for passed width. The unnecessary call can be skipped by moving the setText()/setPage() calls to be run after container’s pack() or setSize(). Or we can ignore them replacing the layout() method of root view. For HTMLEditorKit I used the BlockView extension to ignore it. See example:

        protected void layout(int width, int height) {
            long start=System.currentTimeMillis();
            if (width<Integer.MAX_VALUE) {
                super.layout(width, height);
            }
            long end=System.currentTimeMillis();
            System.out.println("w="+width+" h="+height+" time="+(end-start));
        }

    Full source code of EditorKit and view see below.

  4. Artificial LableView fragments for speedy measuring.

    When “i18n” property of HTML Document is set (normally it happens when multiple languages are used and there are LTR and RTL languages in the Document e.g. English and Hebrew) an alternative GlyphPainter is used – GlyphPainter2. The GlyphPainter2 is based on TextLayout which is used to measure, render and navigate in text. The default GlyphPainter1 uses one static instance of painter in all cases. As against GlyphPainter2 instance is created for each LabelView when we have to measure or render. During paragraph’s layout multiple LabelViews are created, measured and broken into multiple fragments to fit rows.

    The main problem here is measuring and layout of big paragraphs. We measure a huge label and found that it’s bigger than available width. Then we break the label leaving a fragment which fits the row width. Then again create label from the rest and break it again. As result of such process paragraph layout is very slow. To fix this I created a custom ParagrahView with replaced FlowStrategy which breaks big libels into fragments before measuring them. Using the approach increases speed of layout. In my test cases:

    1. Using default ParagraphView setText() took ~2.5 sec and layout() ~ 2 sec when the “i18n” property is set.
    2. When the artificial breaks are done in custom ParagraphView setText() took ~ 0,5 sec and layout() ~ 0,3 sec.

    The custom FlowStrategy works in paragraph like this:

        public static class HTMLFlowStrategy extends FlowStrategy {
            protected View createView(FlowView fv, int startOffset, int spanLeft, int rowIndex) {
                View res=super.createView(fv, startOffset, spanLeft, rowIndex);
                if (res.getEndOffset()-res.getStartOffset()>MAX_VIEW_SIZE) {
                    res = res.createFragment(startOffset, startOffset+MAX_VIEW_SIZE);
                }
                return res;
            }
        }

In conclusion: If neither of the advice above help try to play with custom views, figure out which views took most of time and replace them with custom ones.

NOTE: If you have your own experience how to make it faster please share/discuss your ideas and I’ll try describe them here.


import javax.swing.text.Element;
import javax.swing.text.View;
import javax.swing.text.html.BlockView;
 
public class HTMLBlockView extends BlockView {

    public HTMLBlockView(Element elem) {
        super(elem,  View.Y_AXIS);
    }
 
    protected void layout(int width, int height) {
        long start=System.currentTimeMillis();
        if (width<Integer.MAX_VALUE) {
            super.layout(width, height);
        }
        long end=System.currentTimeMillis();
        System.out.println("w="+width+" h="+height+" time="+(end-start));
    }
}

import javax.swing.text.Element;
import javax.swing.text.View;
import javax.swing.text.FlowView;
 
public class HTMLParagraphView extends javax.swing.text.html.ParagraphView {

    public static int MAX_VIEW_SIZE=100;
 
    public HTMLParagraphView(Element elem) {
        super(elem);
        strategy = new HTMLParagraphView.HTMLFlowStrategy();
    }
 
    public static class HTMLFlowStrategy extends FlowStrategy {
        protected View createView(FlowView fv, int startOffset, int spanLeft, int rowIndex) {
            View res=super.createView(fv, startOffset, spanLeft, rowIndex);
            if (res.getEndOffset()-res.getStartOffset()> MAX_VIEW_SIZE) {
                res = res.createFragment(startOffset, startOffset+ MAX_VIEW_SIZE);
            }
            return res;
        }

    }
    public int getResizeWeight(int axis) {
        return 0;
    }
 
}

import javax.swing.text.html.*;
import javax.swing.text.html.ParagraphView;
import javax.swing.text.*;
 
public class MyEditorKit extends HTMLEditorKit {

    ViewFactory factory=new MyViewFactory();
 
    public ViewFactory getViewFactory() {
        return factory;
    }
 
    class MyViewFactory extends HTMLFactory {
        public View create(Element elem) {
            AttributeSet attrs = elem.getAttributes();
            Object elementName = attrs.getAttribute(AbstractDocument.ElementNameAttribute);
            Object o = (elementName != null) ? null : attrs.getAttribute(StyleConstants.NameAttribute);
            if (o instanceof HTML.Tag) {
                HTML.Tag kind = (HTML.Tag) o;
                if (kind == HTML.Tag.HTML) {
                    return new HTMLBlockView(elem);
                }
                else if (kind == HTML.Tag.IMPLIED) {
                    String ws = (String) elem.getAttributes().getAttribute(CSS.Attribute.WHITE_SPACE);
                    if ((ws != null) && ws.equals("pre")) {
                        return super.create(elem);
                    }
                    return new HTMLParagraphView(elem);
                } else if ((kind == HTML.Tag.P) ||
                        (kind == HTML.Tag.H1) ||
                        (kind == HTML.Tag.H2) ||
                        (kind == HTML.Tag.H3) ||
                        (kind == HTML.Tag.H4) ||
                        (kind == HTML.Tag.H5) ||
                        (kind == HTML.Tag.H6) ||
                        (kind == HTML.Tag.DT)) {
                    // paragraph

//                    return new ParagraphView(elem);
                    return new HTMLParagraphView(elem);
                }
            }
            return super.create(elem);
        }
 
    }
 
}

The test application class:

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
public class App {

    static String testText=null;
    public App() {
    }
 
    public static void main(String[] args) {
        App frame=new App();
        frame.init().setVisible(true);
    }
 
    public JFrame init() {
        JFrame frame = new JFrame("Performance test");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        final JEditorPane editorCustom = new JEditorPane();
        editorCustom.setEditorKit(new MyEditorKit());
 
        final JScrollPane scroll = new JScrollPane(editorCustom);
        editorCustom.getDocument().putProperty("i18n", Boolean.TRUE);
        setText(editorCustom, "");
        frame.getContentPane().add(scroll);
 
        JToolBar tb=new JToolBar();
        JButton btnSetText=new JButton("Set text");
        tb.add(btnSetText);
        btnSetText.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                final int old=scroll.getVerticalScrollBarPolicy();
                scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        setText(editorCustom, "");
                        scroll.setVerticalScrollBarPolicy(old);
                    }
                });
            }
        });
        JButton btnClear=new JButton("Clear");
        tb.add(btnClear);
        btnClear.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                editorCustom.setText("");
            }
        });
        frame.getContentPane().add(tb, BorderLayout.NORTH);
 
        frame.setBounds(0,0,400,400);
        frame.setLocationRelativeTo(null);
        return frame;
    }
 
    private void setText(JEditorPane editor, String txt) {
        long t1=System.currentTimeMillis();
        editor.setText(getTestText());
        long t2=System.currentTimeMillis();
        System.out.println(txt+" setText time="+(t2-t1));
    }
 
    public static String getTestText() {
        if (testText ==null) {
            String before="<html>\n" +
                    "<body>\n" +
                    "<p>\n" +
                    "Some text<br>\n"+
//                    "<img src=\"test.jpg\">\n" +
                    "</p>\n";
 
            String mid= "<p>\n" +
                    "It's a long long long long long long long long long long" +
                    " long long long long long long long long long long long long" +
                    " long long long long long long long long long long long long" +
                    " long long long long long long long long long long long long" + 
                    " long long long long long long long long long long long long" +
                    " long long long long long long long long long long long long"
                    " long long long long long text" +
                    "</p>\n";
            StringBuffer buff=new StringBuffer(before);
            for (int i=0; i<1000; i++) {
//            for (int i=0; i<10000; i++) {
                buff.append(mid);
            }
            String end= "</body>\n" +
                    "</html>";
            buff.append(end);
 
            System.out.println("HTML text length="+buff.length());
            testText = buff.toString();
        }
 
        return testText;
    }
}

Back to Table of Content